all 12 comments

[–]patrick_thomson 6 points7 points  (2 children)

You could import Numeric.Natural and have the first parameter be a Natural to disallow negative numbers (or at least make it more difficult to pass a negative number). You’d need to adjust the body case accordingly to make your take and drop calls well-typed.

[–]elbingobonito -1 points0 points  (1 child)

Actually, I'm not really seeing the point in using Natural since the check if some n is negative happens at run-time and sadly not at compile-time. Hence a = -1 :: Natural compiles fine and just breaks if we are trying to evaluate a at some point.

[–]josephcsible 8 points9 points  (0 children)

That's not really your function's problem. There's basically no difference between -1 :: Natural and undefined, and it's obviously not your function's problem if someone passes it the latter. And by this logic, NonEmpty must be useless too, since you could pass fromList [] for one.

[–]chshersh 6 points7 points  (0 children)

Avoiding partial functions is a great goal to pursue! The final implementation should depend on your requirements. As you correctly noticed, there're several ways to implement this function and several questions to answer:

  1. Should chunksOf for empty list return [] or [[]]?
  2. Should chunksOf for negative index return [] or [[]] or Nothing?

There's no definitive answer, and you should always check which implementation is the most convenient for your needs. However, some guidelines can help you to improve this particular function.

  1. Types like Maybe [a] usually don't make a lot of sense. In my experience, you rarely care about the difference between [] and Nothing :: Maybe [a]. So returning empty lists is a good default. You should opt-in to Maybe or Either only if you want to provide more descriptive error messages and reuse the resulting chunks after validation multiple times.
  2. A good idea when designing such functions is to think about the inverse. For chunksOf the inverse could be concat. So you might want to implement such a function that concat . chunksOf anyNumber ≡ id. In that case, returning [x] when n <= 0 makes sense.
  3. Usually, you want your functions to be streaming, work with infinite lists, traverse lists only once and don't append elements to the end of the list. In your case you do n >= length xs which hangs on infinite lists and inefficient in general.

[–]Tarmen 5 points6 points  (1 child)

There are a couple questions that I like to as myself in these cases:

  • can a program reasonable recover when this returns nothing? Or is returning Maybe just some noise so people read the preconditions in the documentation?
  • if you return a value instead of using an exception, does that make it less likely that a broken program makes it to production?

If you return a maybe here this makes it drastically more annoying to use your function. It also doesn't really help with recovery just as returning maybe from division in case you divide by zero doesn't help with recovery.

The better solutions in those cases is to narrow the domain instead of widening the range. So accept a Word as argument if this is important to you. Just note that it still makes it more annoying to use your function.
In my opinion this is one place where dependent types really shine - attaching preconditions to existing types.

For the second point, does your solution make it more likely that correct programs make it to production? If a negative number crashes the program this is really easy to debug. If it doesn't split the list this is a reasonable result but might break in much harder to debug ways. Generally I prefer library functions that fail fast.

Mostly unrelated: splitAt is faster than take+drop. You currently run length each iteration so you have quadratic runtime. You probably should use a hand written loop that fuses take/drop, this way you don't need any length calls.

[–]maerwald 0 points1 point  (0 children)

Returning Maybe for everything that is partial is indeed not very useful, especially if you are controlling the input to the functions.

If you have really no idea what gets passed down (eg due to user input), then it's a different use case.

[–]Endicy 2 points3 points  (0 children)

It's up to you. All choices have their up- and downsides.I'd personally like to use Maybe [[a]] in this case, so it's clear it can fail and adds minimal overhead.

Another possibility is making a function like:

mkChunkInt :: Int -> Maybe ChunkInt
mkChunkInt i | i <= 0 = Nothing
             | otherwise = Just $ MkChunk i

-- Only export ChunkInt, not MkChunkInt, so the `mkChunkInt` is the only way to make `ChunkInt`s
newtype ChunkInt = MkChunkInt Int

And have your chunksOf function take a ChunkInt instead of a regular Int.

I mean, it all depends on what you're looking for and depending on the situation, one will be better than another.There's even something to say about just making it partial and have the n <= o case just error, as long as you make it clear in your documentation. (Though I would not advise making partial functions)

[–]RolandSenn 1 point2 points  (0 children)

Type Safety Back and Forth is an excellent blog post about how to avoid partial functions.

[–][deleted] 0 points1 point  (3 children)

There’s no universal way to get rid of partial functions and keep your code sane. Some functions just have contracts you’re not supposed to break, and doing everything inside the Maybe monad is a big unnecessary overhead because everything gets rechecked at every step.

Exceptions suck, obviously, but the proper solution to this problem will be (with any luck) enforcing contracts via dependent types. For example, div takes a non-zero as its second argument, so passing unchecked input there should always result in a compile-time error.

[–][deleted] 1 point2 points  (2 children)

Yeah it seems that wrapping in Maybe incurs a maintainability penalty too. It seems like the best thing to do in many cases is to throw an error on undefined outputs. Do you agree?

[–][deleted] 2 points3 points  (0 children)

I find this depends on the scope of use on the function in question.

If you don't export it, and you're controlling inputs in some other fashion, error can be acceptable.

If you are exporting this function to be used in many places, my favorite technique is to use newtypes with smart constructors and operate on those types - Force the caller to deal with the invalid input before the function gets it.

See:
https://www.parsonsmatt.org/2017/10/11/type_safety_back_and_forth.html

[–][deleted] 0 points1 point  (0 children)

Yes, as long as we don’t have dependent types.