list.map (lambda f => readFile! f) list.map (lambda f => readFile! f)That's why Scala introduces so called "capture checking" to make direct style effect systems safe.
https://docs.scala-lang.org/scala3/reference/experimental/cc...
To understand the problem better see:
https://www.inner-product.com/posts/direct-style-effects/#ca...
Well... yeah? It looks like readFile (a Task) is sufficiently thunk-like to guess that that's the case.
I program in this still regularly. What bad outcomes should I expect?
I imagine if you did your function, it would return a list of unexecuted Tasks.
Just by guessing, the way to do it (actually read the files) would probably look like:
list.mapTask (lambda f => readFile! f)
or Tasks.map list (lambda f => readFile! f) Task.seq \f -> File.read! f
https://www.roc-lang.org/packages/basic-cli/Task#seqI also don't get how running
list.map (lambda f => readFile! f)
would result in a list of Tasks. `readFile` needs already to return a Task as it's an I/O operation, right? Appending the exclamation mark will CPS transform that Task value by wrapping it into (possibly nested) calls to `Task.await`, as I understand the article. But CPS inside lambdas is problematic as you need to also CPS transform the lambda, more or less rewriting your whole program "inside out" (all up the whole call stack!). Also every higher order function would need to be there in two variants. HOFs need to get "shifted" manually for that. That doesn't scale.I mean, it's not impossible to implement something like that. But it's not trivial. The problem with the HOFs remains still. See for example CPS-async in Scala:
The article briefly mentioned this edge case:
> Since this is the last task in a chain, the ! doesn't do anything and isn't necessary…so we just drop it during desugaring instead of giving a compiler error or generating an unnecessary Task.await. This allows for a more consistent visual style, where async I/O operations always end in the ! suffix, but doesn't have any runtime cost.
That's why it would return a list of Tasks.
Also, how is the expression `doSomething!` typed then? Does it have different types depending on where it's written?
And how do I actually write something `traverse`-like? How to actually write
list.forEach (lambda f => readFile! f)
than, if I really want the effect to run inside the lambda body?That's the contract of 'map' in any FP. Turn a container of A into a container of B, and do nothing else (in this case - no telling tasks to start executing).
> Appending the exclamation mark will CPS transform that Task value ... is problematic as you need to also CPS transform the lambda
You may be overthinking it. I believe (!) is a syntax-only transformation which happens very early in the compiler pipeline. And I don't think you need to invoke CPS here:
CPS From [https://matt.might.net/articles/cps-conversion/]
The M function only has to watch for lambda terms. When it sees a lambda term, it adds a fresh continuation parameter, $k, and then transforms the body of the lambda term into continuation passing style, asking it to invoke $k on the result. Variables are unchanged
In Roc (and Haskell, Scala, Idris, ...) you don't need to generate a fresh $k under the hood, because it's already right there in the surface language (in this case, foo): foo = mytask! "abc"
is already equivalent to: mytask "abc" (\foo -> ... )
> See for example CPS-async in Scala:Why didn't you mention Scala earlier :D. This is the same trick as Scala for-comprehensions. Here's an example in that repo: https://github.com/rssh/dotty-cps-async/blob/36f2e3b61542b39...
> Also every higher order function would need to be there in two variants.
Literally every HOF? Nah, but you're right that there is duplication - and there needs to be, since there is a distinction between:
convert a *list of filenames* (input) into a *list of Tasks* (output) which will read those filenames
convert a *list of filenames* into Tasks which will read those filenames, and return their *contents* (output)
If you have List and Task modules, then "do this List of Tasks" obviously has to live in List's source code or Task's source code. It's an N*M implementations problem, but fuck it. Computers do Lists of Tasks so it's worth implementing.Haskell does a little better non the non-duplication front. By going down the typeclass/monad route, you can put the "do this List of Monads" in List.hs, and "Task is a Monad" in Task.hs, and you're all set. But it still makes a distinction between 'map the list' and 'map the list and return the results of running the monads inside it'. It calls them map and mapM respectively, and also has mapM_ which carries out the effects but also ignores the output.
Anyway, back to my Java day job where the duplication issue in the stdlib is solved by just ... leaving out functionality. Let's see how we tackle the List of Tasks issue: https://stackoverflow.com/questions/30025428/convert-from-li...