base icon indicating copy to clipboard operation
base copied to clipboard

It's easy to silently mess up Stream.flatMap calls

Open ceedubs opened this issue 2 years ago • 1 comments

This is a fun one that I just ran into. What is the result of this watch expression?

> Stream.fromList ["foo", "bar"]
    |> Stream.flatMap (s -> Stream.fromList (toCharList s))
    |> Stream.toList!

If you guessed [] then you are right!

The issue is that the code should use Stream.fromList! (toCharList s) instead of Stream.fromList (toCharList s).

Let's look at the signature of Stream.flatMap:

  Stream.flatMap :
    (a ->{e, Stream b} any) -> '{e, Stream a} r -> '{e, Stream b} r

In the example, the function that we are passing in has the signature Char -> {} ('{Stream Char} ). So Stream.flatMap views this as a function that takes a Char, doesn't emit any stream elements, and then returns an ignored value which happens to be a thunk that would emit a stream of characters should you run it instead of ignore it.

I think that a simple solution would be to make Stream.flatMap require () as the return type of the mapping function:

  Stream.flatMap :
    (a ->{e, Stream b} ()) -> '{e, Stream a} r -> '{e, Stream b} r

This means that occasionally you might need to add an ignore to your function, but I think that's a small price to pay to avoid silent issues that cause your code to not at all do what you would expect.

NOTE: the same applies to Stream.flatMap!.

cc @anovstrup who has done a lot of great Stream work.

ceedubs avatar Sep 06 '23 14:09 ceedubs

Hah I now retract my "Sounds fine to me" that I commented here.

ceedubs avatar Sep 06 '23 14:09 ceedubs