I read through the tutorial this morning and the record builder example, and I'm left confused about what the left arrow does (<-
). Is there more about it that I can read?
It is just syntax sugar for lambda.
x <- Task.await myTask
# more code
Is equivalent to:
Task.await myTask \x ->
#more code
Avoids indentation hell. That said, it will likely be removed in favor of only allowing !
.
Hm, ok. I'm not quite getting it yet. So if you use <-
multiple times in one expression (like in the Record Builder example) it builds a lambda with more than one parameter?
The : <-
and <-
work in different ways, others have been confused by it as well, but like Brendan said, we will likely get rid of the (standalone) <-
.
Ah, ok. I guess I'm asking about : <-
then. I hadn't encountered standalone <-
.
initIDCount {
aliceID: <- incID,
bobID: <- incID,
trudyID: <- incID,
}
|> extractState
Converts to:
initIDCount (\aID -> \bID -> \cID -> { aliceID: aID, bobID: bID, trudyID: cID })
|> incID
|> incID
|> incID
|> extractState
Is there a specific part that does not make sense?
Knowing that : <-
is a distinct "operator" and not just assigning a usage of <-
helps. I'm still not quite sure how I can apply it to different situations, but I think I might just need to sit with a bit longer
Yes, the example is quite artificial, it's very useful for use with the Roc postgres library
Here’s an article about a cool CLI arg parser built using record builders: https://sammohr.dev/blog/announcing-weaver
Record Builders are basically syntax sugar for applicatives if you know those from other FP languages
Unfortunately I have very little experience with other FP languages. I suspect that an arg parser is actually kind of close to what I'm trying to build though, so I'll read that article. (I'm trying to build something that is sort of like the builder pattern that I've seen in Rust, but with fields that can be defined as delayed functions)
I have been using roc for a long time, find record builders to still be magic even if I can desugar them
I think that applicatives even if written out can be hard to follow.
Someone(@Agus Zubiaga?) had a really good animation at the zig milan meetup in a lightning talk. Was super useful cause it showed the unwrapping as the applicatives where used.
Yeah, we have some ideas on how to simplify them to work in terms of map2 instead of applicatives. I’ll probably look into that later in the year.
Mythmon said:
Unfortunately I have very little experience with other FP languages.
My first programming language was functional, and I haven’t wrapped my head around : <-
yet. So don’t worry, you’re definitely not alone
That's reassuring. If I could take advantage of the attention here, this is the thing I was trying to use : <-
for, written out verbosely. I was trying to understant : <-
as something like Rust's ?
, which I don't think is right now
Slot a : [Undefined, Final a, Delayed a]
Character : {
name : Str,
xp : U8,
level : U8,
}
PartialCharacter : {
name : Slot Str,
xp : Slot U8,
level : Slot U8,
}
finalize : PartialCharacter -> [Ok Character, Err Str]
finalize = \pc ->
when (pc.name, pc.xp, pc.level) is
(Final name, Final xp, Final level) -> Ok { name, xp, level }
(Undefined, _, _) -> Err "Missing name"
(_, Undefined, _) -> Err "Missing xp"
(_, _, Undefined) -> Err "Missing level"
(Delayed _, _, _) | (_, Delayed _, _) | (_, _, Delayed _) -> Err "TODO Can't handle delayed values yet"
I expect the number of fields on Character
to grow a lot (the TypeScript code I'm rewriting already has 20+ fields, and isn't even complete). Is there a better way to structure this that won't require huge when definitions?
It would be great if finalize could be something like
finalize = \pc ->
something {
name: <- ensure pc.name "name",
xp: <- ensure pc.xp "xp",
level: <- ensure pc.level "level",
}
Yeah, I think that should work. Let me give it a try.
@Mythmon This should work:
finalize : PartialCharacter -> Result Character Str
finalize = \pc ->
Ok {
name: <- ensure pc.name "name",
xp: <- ensure pc.xp "xp",
level: <- ensure pc.level "level",
}
ensure : Slot a, Str -> (Result (a -> b) Str -> Result b Str)
ensure = \slot, name ->
\prev ->
when (prev, slot) is
(Ok advance, Final value) -> Ok (advance value)
(Ok _, Undefined) -> Err "Missing $(name)"
(Ok _, Delayed _) -> crash "todo"
(Err err, _) -> Err err
expect finalize { name: Final "Alice", xp: Final 42, level: Final 3 } == Ok { name: "Alice", xp: 42, level: 3 }
expect finalize { name: Final "Alice", xp: Final 42, level: Undefined } == Err "Missing level"
Here's the desugaring animation that @Brendan Hansknecht mentioned:
record-builder-desugaring.mov
I recommend trying the desugared version yourself and figuring out the types at each level of the pipeline
Applicatives can be quite mind bending because the type wraps this curried function that’s applied at each level
As I said earlier, I’d like to try a map2 approach later because that’s much easier to grasp
Last updated: Jul 06 2025 at 12:14 UTC