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
ResultandTaskto 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.awaits
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: Nov 09 2025 at 12:14 UTC