I've been looking at the new record builder syntax, and not sure if it is possible to use it currently. I think we need a new function added to basic-cli
to enable the applicative API, but not sure. Am I correct in saying that we need the following function to be able to use record builders with Task
?
Task.apply : Task a err -> Task (a -> b) err -> Task b err
My goal was to understand what an applicative API looks like, so I can understand where this language feature may be applied.
I've made an example below, it's contrived but I wanted to just get something basic working.
% roc run Experiments/Applicative.roc
Apples: Granny Smith, Pink Lady, Golden Delicious
Oranges: Navel, Blood Orange, Clementine
app "applicative"
packages {
pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.2/tE4xS_zLdmmxmHwHih9kHWQ7fsXtJr7W7h3425-eZFk.tar.br",
}
imports [
pf.Stdout,
pf.Task.{Task},
]
provides [main] to pf
main =
myrecord : Task { apples : List Str, oranges : List Str } []
myrecord = Task.succeed {
apples: <- getFruit Apples |> apply,
oranges: <- getFruit Oranges |> apply,
}
{apples,oranges} <- myrecord |> Task.await
"Apples: "
|> Str.concat (Str.joinWith apples ", ")
|> Str.concat "\n"
|> Str.concat "Oranges: "
|> Str.concat (Str.joinWith oranges ", ")
|> Stdout.line
getFruit : [Apples, Oranges] -> Task (List Str) []
getFruit = \request ->
when request is
Apples -> Task.succeed ["Granny Smith", "Pink Lady", "Golden Delicious"]
Oranges -> Task.succeed ["Navel", "Blood Orange", "Clementine"]
apply : Task a err -> (Task (a -> b) err -> Task b err)
apply = \first -> \second ->
result <- second |> Task.attempt
when result is
Ok f -> Task.map first f
Err err -> Task.fail err
Ok, I think this is really cool. It also works with Result
:smiley:
% roc run Experiments/Applicative.roc
Insufficient fruit available
app "applicative"
packages {
pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.2/tE4xS_zLdmmxmHwHih9kHWQ7fsXtJr7W7h3425-eZFk.tar.br",
}
imports [
pf.Stdout,
]
provides [main] to pf
main =
myrecord : Result { apples : List Str, oranges : List Str } [LackOfFruit]
myrecord = Ok {
apples: <- getFruit Apples |> apply,
oranges: <- getFruit Oranges |> apply,
}
when myrecord is
Ok {apples,oranges} ->
"Apples: "
|> Str.concat (Str.joinWith apples ", ")
|> Str.concat "\n"
|> Str.concat "Oranges: "
|> Str.concat (Str.joinWith oranges ", ")
|> Stdout.line
Err LackOfFruit ->
Stdout.line "Insufficient fruit available"
getFruit : [Apples, Oranges] -> Result (List Str) [LackOfFruit]
getFruit = \request ->
when request is
Apples -> Ok ["Granny Smith", "Pink Lady", "Golden Delicious"]
# Oranges -> Ok ["Navel", "Blood Orange", "Clementine"]
Oranges -> Err LackOfFruit
apply : Result a err -> (Result (a -> b) err -> Result b err)
apply = \first -> \second ->
when second is
Ok f -> Result.map first f
Err err -> Err err
So, I guess my question here is probably more suited for #ideas but, should we add the applicative "apply" to Result
and Task
to enable the use of record builder?
and a follow up question, what should the implementation look like, I just used first
and second
as I have no idea what to name these variables.
Luke Boswell said:
So, I guess my question here is probably more suited for #ideas but, should we add the applicative "apply" to
Result
andTask
to enable the use of record builder?
I think it's a good idea to should start an #ideas
for Result
:thumbs_up:
for Task
, we 100% definitely want to, and I like the name Task.batch
for it. If others prefer a different name, we could discuss, but I'm also fine with saying we could go ahead with adding Task.batch
to basic-cli
and implement it by chaining together Task.await
s
having Task.batch
run concurrently requires #ideas > Task as builtin (apply
and map2
are two sides of the same coin, and each can be implemented in terms of the other) but we can still offer the API right now even if it doesn't run concurrently, and then upgrade it behind the scenes in the future to make it concurrent!
also I think we might as well add map2
for both Result
and Task
while we're at it :smiley:
Luke Boswell said:
myrecord : Result { apples : List Str, oranges : List Str } [LackOfFruit] myrecord = Ok { apples: <- getFruit Apples |> apply, oranges: <- getFruit Oranges |> apply, }
Interesting - I never even considered this usage!
When you're building a record anyway, it's a more concise alternative to Result.try
and backpassing:
myrecord : Result { apples : List Str, oranges : List Str } [LackOfFruit]
myrecord =
apples <- getFruit Apples |> Result.try,
oranges <- getFruit Oranges |> Result.try,
Ok { apples, oranges }
@Richard Feldman do the function arguments to batch
have special names? I am using first
and second
here which seem wrong
that's a good question! I'm not aware of any precedent for good names here, to be honest :sweat_smile:
it's a type that is all of these things:
hopefully we can figure some out!
Posting in #ideas
Last updated: Jul 05 2025 at 12:14 UTC