Stream: contributing

Topic: Record Builder Syntax


view this post on Zulip Agus Zubiaga (May 02 2023 at 21:24):

I'm working on a few things that would benefit from Proposal: Record Builder Syntax. In my opinion, this would make Roc stand out from other FP languages where applicatives are often leveraged but feel somewhat clunky.

I already looked into how this could be added, and I would love to take a stab at implementing it.

@Richard Feldman Is this at a stage where you'd consider accepting a PR or is it too early for that?

view this post on Zulip Richard Feldman (May 02 2023 at 21:30):

totally happy to accept a contribution for it! :smiley:

view this post on Zulip Richard Feldman (May 02 2023 at 21:31):

the only question mark in my mind is whether we should have it desugar to apply or map2 (each can be implemented in terms of the other, but I haven't thought through the pros/cons of choosing one vs the other) but that's the very last step of the feature, so I don't think it's a blocker for getting started

view this post on Zulip Agus Zubiaga (May 02 2023 at 21:48):

Interesting. If we desugared to map2 the "sugared" source would be different, right?

usersAndPosts = Task.collect {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

In the example, apply is provided as Task.batch.

view this post on Zulip Richard Feldman (May 02 2023 at 22:36):

hm yeah, true

view this post on Zulip Richard Feldman (May 02 2023 at 22:36):

yeah maybe map2 doesn't make sense :stuck_out_tongue:

view this post on Zulip Richard Feldman (May 02 2023 at 22:37):

it just occurred to me recently that it was an option because it can be losslessly transformed to apply, but thinking about it some more, it doesn't really seem to have any upsides here!

view this post on Zulip Richard Feldman (May 02 2023 at 22:37):

so let's stick with apply

view this post on Zulip Agus Zubiaga (May 02 2023 at 23:16):

Yeah, apply seems more straightforward for this use case.

view this post on Zulip Agus Zubiaga (May 02 2023 at 23:16):

Ok, cool. I'll get started whenever I get some free time :smiley:

view this post on Zulip Richard Feldman (May 02 2023 at 23:33):

awesome, feel free to post here if you have any questions!

view this post on Zulip Georges Boris (May 02 2023 at 23:33):

I'm not sure if this is the place for it but I just read the design doc for this feature and left wondering if there isn't something even better we could figure out without the downsides listed.

for instance, does it make sense to use anything other than Task.batch for each of the items? I would think that Task.await would make it sequential - though it wouldn't right? since one of the design goals of this syntax is to make people understand they can't use ordering.

it seems like this approach might make people have a headache trying to compute values inside the same collect statement, even if it doesn't work it kinda looks enough like backpassing for people to think it would work...

with that in mind, any computed values or sequential tasks would need to happen in a follow-up step, right? wouldn't it be better to restrict it more and people could use a Task.map if they wanted to compute something or Task.andThen if they wanted to do something after all requests are done.

something like

Task.batch {
  users: Http.get "/users",
  posts: Http.get "/posts"
}

the syntax sugar would be for the function Task.batch itself - it requires a record of tasks.

view this post on Zulip Brendan Hansknecht (May 02 2023 at 23:53):

IIUC, Task.await would fail to typecheck.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:00):

Related to his above question, why do we need Task.collect at all. We should even be able to do:

usersAndPosts = Task.succeed {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

It could desugar to:

Task.succeed (\users -> \posts -> { users, posts })
|> Task.batch (Http.get "/users")
|> Task.batch (Http.get "/posts")

I am not sure what adding in an extra layer of indirection and wrap function does for us.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:04):

Alos, @Georges Boris, your above proposal would have no way to distinguish from a regular record without blessing Task.batch, it needs to at least keep something like <- so that we know to apply syntax sugar.

view this post on Zulip Georges Boris (May 03 2023 at 00:11):

so would this syntax work for any other record constructor? from the doc I thought it was explicitly related to parallel tasks - so this would also be useful for something like this?

D.succeed {
  name <- D.field "name" D.string  |> D.required,
  email <- D.field "email" D.string |> D.optional
}

(trying to mimick elm's json decode pipeline api)

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:16):

Interesting. I mean it would build:

D.succeed (\name -> \email -> {name, email}
|> D.required (D.field "name" D.string)
|> D.optional (D.field "email" D.string)

the type of required and optional would be something like:

required: D a err, D (a -> b) err, D b err

optional: D a err, D (a -> b) err, D (Option b) []

Where optional would on failure return None and clear out the error. While required would propagate the error.

I think all the types could work out at a minimum.

view this post on Zulip Agus Zubiaga (May 03 2023 at 00:17):

@Georges Boris Yes, it would work for any applicative API. For example, I want to use it to build type-safe SQL selections. You could also use it to build a CLI arguments parser.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:17):

You would just need something that can actually execute D just like we have platforms executing Task. So I think yes, it would enable that.

view this post on Zulip Agus Zubiaga (May 03 2023 at 00:18):

@Brendan Hansknecht Good point about not needing Task.collect

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:20):

Oh, but D.string would not really work. But some other api should work. As in, D.string could work, but then email would not be a Str instead it would be a wrapped [String Str]. So you would then have to match on the type even though you know the only type it could be. I think instead you would need D.fieldStr and same for other types to make the api play nicely with types in roc.

view this post on Zulip Georges Boris (May 03 2023 at 00:21):

it seems like a nice API that will need a veeery careful error message setup. I can totally see someone getting mixed up with types when trying to mix something like Task.batch and Task.await since the dependency is not as clear as an actual pipeline.

view this post on Zulip Georges Boris (May 03 2023 at 00:21):

(optional actually takes a default value in Elm - my mistake!)

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:24):

Will be interesting to see how this works with Decode in the future. I would assume it would in the end be built on top of Decode, but maybe that is wrong. Not sure if you can currently decode with a default value. I think not.

view this post on Zulip Agus Zubiaga (May 03 2023 at 00:28):

Georges Boris said:

it seems like a nice API that will need a veeery careful error message setup. I can totally see someone getting mixed up with types when trying to mix something like Task.batch and Task.await since the dependency is not as clear as an actual pipeline.

That's true. On the other hand, by having a special syntax for applicatives, we can produce much more insightful error messages than the other languages.

view this post on Zulip Agus Zubiaga (May 03 2023 at 00:39):

Brendan Hansknecht said:

Related to his above question, why do we need Task.collect at all. We should even be able to do:

usersAndPosts = Task.succeed {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

It could desugar to:

Task.succeed (\users -> \posts -> { users, posts })
|> Task.batch (Http.get "/users")
|> Task.batch (Http.get "/posts")

I am not sure what adding in an extra layer of indirection and wrap function does for us.

This is a good point. I can't come up with anything the layer of indirection would allow us to do, that we couldn't without it.

view this post on Zulip Agus Zubiaga (May 03 2023 at 00:41):

Other than having a more intentional name for it I guess.

view this post on Zulip Richard Feldman (May 03 2023 at 00:47):

the reason for Task.collect is that it makes the sugar a self-contained expression

view this post on Zulip Richard Feldman (May 03 2023 at 00:47):

in other words, with Task.collect, the sugar is all inside the curly braces and everything outside the curly braces is normal Roc

view this post on Zulip Richard Feldman (May 03 2023 at 00:48):

if you want Task.succeed { ... } to replace Task.collect { ... } then you need to say that, despite all appearances, that Task.succeed { ... } is not a function call

view this post on Zulip Richard Feldman (May 03 2023 at 00:48):

rather it's the beginning of some syntax sugar that you only find out is happening if you read what's inside the curly braces

view this post on Zulip Richard Feldman (May 03 2023 at 00:49):

and every other time you see Task.succeed { ... } you know that it's going to evaluate to a Task { ...some fields... } but now it might not

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:50):

I think calling Task.collect any different/an actual function call is wrong. Task.collect doesn't even make sense to be called with those args. (As in either way it is not a true function call and is a rule that a roc user needs to know. If they look at the raw Task.collect, or the raw Task.succeed neither truly makes senses. I think Task.succeed reads closer to the expected output type.)

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:51):

every other time you see Task.succeed { ... } you know that it's going to evaluate to a Task { ...some fields... } but now it might not

This is still true. They syntax sugar always generates a record.

view this post on Zulip Richard Feldman (May 03 2023 at 00:54):

Brendan Hansknecht said:

Related to his above question, why do we need Task.collect at all. We should even be able to do:

usersAndPosts = Task.succeed {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

It could desugar to:

Task.succeed (\users -> \posts -> { users, posts })
|> Task.batch (Http.get "/users")
|> Task.batch (Http.get "/posts")

if you desugar the |>s out, the Task.batch calls are the outermost calls.

in other words, in this example we started with Task.succeed { ... } and ended up with (after desugaring) Task.batch (Task.batch (Task.succeed ...)))

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:55):

I personally like the parallel between:

Task.succeed {
    user: getUser someData
    email: loadEmail a b c
}

and:

Task.succeed {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

Both give you the same output Task {user: Str, email: Str} err, but one is in parallel and using sub tasks.

view this post on Zulip Richard Feldman (May 03 2023 at 00:56):

that's a compelling point!

view this post on Zulip Richard Feldman (May 03 2023 at 00:56):

:thinking: I guess |> itself is actually precedent for adding wrapping calls to an expression after the fact

view this post on Zulip Brendan Hansknecht (May 03 2023 at 00:57):

Of course, I think it will confuse users that some reason they have to transform to this syntax if there are dependencies:

users <- Http.get "/users" |> Task.await
posts <- Http.getPosts users "/posts" |> Task.await

Task.succeed {users, posts}

view this post on Zulip Richard Feldman (May 03 2023 at 00:57):

so it's already not necessarily the case that if you see Task.succeed { ... } you know that a Task.succeed will be the first call there; you already have to keep reading to see if there's a |> coming in order to know what will be evaluated first

view this post on Zulip Richard Feldman (May 03 2023 at 00:57):

and actually I guess that's true of binary operators in general, not just |> :big_smile:

view this post on Zulip Brendan Hansknecht (May 03 2023 at 01:04):

Oh, another expansion to the syntax, could it also support this:

users <-getUsers |> Task.await

Task.succeed {
    users,
    emails <-getEmails users |> Task.batch,
    posts <-getPosts users |> Task.batch,
}

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:05):

Yes! That was going to be my next question.

view this post on Zulip Richard Feldman (May 03 2023 at 01:07):

I suppose, although at least one of them needs to be <- :sweat_smile:

view this post on Zulip Richard Feldman (May 03 2023 at 01:07):

wait, actually I don't think that works

view this post on Zulip Richard Feldman (May 03 2023 at 01:07):

because it's not a |> Task.batch

view this post on Zulip Richard Feldman (May 03 2023 at 01:07):

I definitely do not think we should allow mixing and matching await and batch

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:08):

It should work, users in that scope is already the final value, not the Task

view this post on Zulip Richard Feldman (May 03 2023 at 01:08):

oh wait nm I misunderstood

view this post on Zulip Richard Feldman (May 03 2023 at 01:08):

so users is sugar for users: users as usual, not users <- users :thumbs_up:

view this post on Zulip Richard Feldman (May 03 2023 at 01:08):

yeah that makes sense!

view this post on Zulip Richard Feldman (May 03 2023 at 01:08):

in that case we should allow users: blah as well (I forget if the doc mentioned that)

view this post on Zulip Richard Feldman (May 03 2023 at 01:09):

cool, I'm on board with both of those changes :thumbs_up:

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:09):

For sure! When I was looking into how I would implement this earlier, I thought we can split the fields set with : (or LabelOnly) from those using <-.

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:10):

The former are kept the same, and the latter get desugared into the pipeline thing.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 01:14):

Yes, exactly what I was thinking

view this post on Zulip Georges Boris (May 03 2023 at 01:17):

could be useful to have a default type alias for applicatives so it is easier to spot which functions are compatible with this feature than looking for the usually confusing type signature... type alias is definitely not the way to go but would be great to spot compatibility easily.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 01:29):

the pattern is:Task a err -> Task (a -> b) err -> Task somethingWithB err

So could be:

ApplicativeTask a b err : Task a err -> Task (a -> b) err

batch: ApplicativeTask a b err -> Task somethingWithB err

view this post on Zulip Brendan Hansknecht (May 03 2023 at 01:29):

Not sure if it helps, but maybe...idk

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:52):

I think we overlooked something:

Task.succeed {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

could not desugar to:

Task.succeed (\users -> \posts -> { users, posts })
|> Task.batch (Http.get "/users")
|> Task.batch (Http.get "/posts")

because we would be applying Task.batch with 2 arguments: Task.succeed (\users -> \posts -> { users, posts }) and (Http.get "/users")

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:54):

It would work in Elm because Task.batch would be partially applied with Http.get .. and then the pipe would apply Task.succeed (..)

view this post on Zulip Brendan Hansknecht (May 03 2023 at 01:57):

Since batch is Task a err -> Task (a -> b) err -> Task b err, it is manually curried.

I think you just need extra parens:

Task.succeed (\users -> \posts -> { users, posts })
|> (Task.batch (Http.get "/users"))
|> (Task.batch (Http.get "/posts"))

view this post on Zulip Agus Zubiaga (May 03 2023 at 01:59):

Oh, parens do that? Let me check

view this post on Zulip Agus Zubiaga (May 03 2023 at 02:00):

CleanShot-2023-05-02-at-22.59.482x.png

view this post on Zulip Brendan Hansknecht (May 03 2023 at 02:01):

I think |> may not work correctly, but something like this should definitely type check:

batch1 = Task.batch (Http.get "/users")
batch2 = Task.batch (Http.get "/posts")

Task.succeed (\users -> \posts -> { users, posts })
|> batch1
|> batch2

view this post on Zulip Brendan Hansknecht (May 03 2023 at 02:01):

Yeah, somehow we need to force application. Not sure if we have a way to do that in roc. Interesting issue.

view this post on Zulip Agus Zubiaga (May 03 2023 at 02:03):

Right. We could desugar to:

(Task.batch (Http.get "/posts"))
    (
        (Task.batch (Http.get "/users"))
            (Task.succeed (\users -> \posts -> { users, posts }))
    )

view this post on Zulip Brendan Hansknecht (May 03 2023 at 02:04):

I think this may be a bug in |>, but I could see an argument for either. Personally, I think the parens should cause execution even in a pipeline cause that is what happens when desugared like above.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 02:04):

Cause the |> should desugar to what you wrote above.

view this post on Zulip Agus Zubiaga (May 03 2023 at 02:10):

Yeah, interesting. I guess it could do that, I'm not sure if I'd expect it to, the way I would in Elm with partial application.

view this post on Zulip Brendan Hansknecht (May 03 2023 at 02:15):

Yeah, I'm not sure either, but then it leaves an open question, how do I apply a manually curried function in a pipeline? Feels weird to say it is impossible

view this post on Zulip Brendan Hansknecht (May 03 2023 at 02:15):

I mean I guess you could wrap it |> \x -> (manuallyCurried someParam) x

view this post on Zulip Agus Zubiaga (May 04 2023 at 00:49):

I still have like 10 TODOs but this thing is working!
CleanShot-2023-05-03-at-21.48.502x.png

view this post on Zulip Agus Zubiaga (May 04 2023 at 00:53):

Also formatting:
CleanShot-2023-05-03-at-21.52.33.gif

view this post on Zulip Richard Feldman (May 04 2023 at 00:53):

wowwwwwww, this is amazing!

view this post on Zulip Brendan Hansknecht (May 04 2023 at 01:26):

Oh wow...fast

view this post on Zulip Georges Boris (May 04 2023 at 03:20):

would it be too crazy if the formatter transformed the non-sugared versions into this syntax for consistency? it would also prevent the errors this syntax is trying to help with :nerd:

view this post on Zulip Agus Zubiaga (May 04 2023 at 13:18):

Hm, interesting. I think it'd be hard to detect in a way that wouldn't trigger for other constructs that might look the same (AST wise) but are not applicatives at all.

view this post on Zulip Agus Zubiaga (May 04 2023 at 13:20):

Also, IMHO that'd just be too surprising

view this post on Zulip Agus Zubiaga (May 04 2023 at 13:25):

To me, that'd be similar to having the formatter rewrite a (b c)) to c |> b |> a. I wouldn't want it to do that.

view this post on Zulip Agus Zubiaga (May 05 2023 at 14:02):

I’m pretty busy after work this week, so I’m unlikely to continue coding this until next week. However, I’d like to determine all the cases where we want to allow record builders in.

This is what I have so far:

Allowed

# 1. Applied to a function identifier
succeed {
    a: 123,
    b <- apply getB,
    c <- apply getC,
    d,
}

# 2. Applied to tag
Succeed {
    a <- apply getA,
    b <- apply getB,
}

# 3. Applied to opaque wrapper
@Opaque {
    a <- apply getA,
    b <- apply getB,
}

# 4. Piped
{
    a <- apply getA,
    b <- apply getB,
}
|> succeed

# 5. Applied next to other arguments
succeed 456 {
    a <- apply getA,
    b <- apply getB,
}

# 6. Applied to an expression that returns a function
(getSucceed somehow) {
    a <- apply getA,
    b <- apply getB,
}

Maybe allowed?

# 7. Record update
succeed {
    recordToUpdate &
        a <- apply getA,
        b <- apply getB,
        c: 123,
        d
}

Compile error

# 8. Record builder in a def
builder =
    {
        a <- apply getA,
        b <- apply getB,
    }

succeed builder

# 9. Mulitple record builders applied to the same function
succeed
    {
        a <- apply getA,
        b <- apply getB,
    }
    {
        z <- apply getZ,
    }

Please let me know what you think. Let’s use the numbers to talk about each case.

view this post on Zulip Brendan Hansknecht (May 05 2023 at 14:06):

My gut feeling is 1,2,3 and nothing else. It should only apply to functions directly.

4 is not direct.
5... I am really not sure would need to think about use cases
6 Probably not. We already pointed out that (func arg) does not necessarily apply a function in roc. I think it just also would stick out weirdly and be uncommon.
7 Shouldn't be supported
8 Shouldn't be supported
9 Shouldn't be supported

view this post on Zulip Richard Feldman (May 05 2023 at 15:36):

very cool! :smiley:

I hadn't even thought about making this work for all the things that would be valid in front of a normal record literal (e.g. 2, 3, 4, 5) but I like that if someone tries to do that, it can Just Work the same way as any other expression would :thumbs_up:

view this post on Zulip Richard Feldman (May 05 2023 at 15:36):

I think we definitely should not support record updates (7) at least at first; if there's demand for it in practice, we can have a separate discussion about it and decide later whether to add it

view this post on Zulip Richard Feldman (May 05 2023 at 15:37):

also there have been discussions in the past about potential changes to record update syntax, so committing to it would potentially complicate those discussions unnecessarily

view this post on Zulip Richard Feldman (May 05 2023 at 15:38):

I like 8 being a compiler error; I could see an argument for it returning a partially-applied function, but I suspect that would be more surprising/confusing than useful.

view this post on Zulip Richard Feldman (May 05 2023 at 15:38):

I'm not even sure if there's a design where 9 could work, so :thumbs_up: to compile error there too :big_smile:

view this post on Zulip Brendan Hansknecht (May 05 2023 at 15:55):

I think if 8 doesn't work, 4 shouldn't work. They are the same code, but one is explicit with the name and the other implicit

view this post on Zulip Richard Feldman (May 05 2023 at 15:59):

hm, my main concern with 8 is that you can pass builder around, put it into the repl (what would its type be?), etc.

view this post on Zulip Richard Feldman (May 05 2023 at 15:59):

and those don't really make sense unless you define it to be a partial application or something

view this post on Zulip Richard Feldman (May 05 2023 at 16:00):

hm, actually maybe that idea is worth exploring :thinking:

view this post on Zulip Richard Feldman (May 05 2023 at 16:01):

eh nm, that doesn't make sense to me

view this post on Zulip Richard Feldman (May 05 2023 at 16:02):

I'm also ok with 4 being disallowed for now, and then we can revisit later

view this post on Zulip Brendan Hansknecht (May 05 2023 at 16:23):

Also, should (someFunc someArg) apply a function in Roc? Currently that isn't consistent due to pipelining which ignores parens.

view this post on Zulip Brendan Hansknecht (May 05 2023 at 16:23):

Cause that is what makes me unsure about 6

view this post on Zulip Richard Feldman (May 05 2023 at 16:24):

hm, what's an example of pipelining ignoring parens in that context?

view this post on Zulip Brendan Hansknecht (May 05 2023 at 16:27):

x
|> someFunc y

# becomes
someFunc x y

# and this
x
|> (someFunc y)

# becomes
(someFunc x y)

# So you have to write
x
|> \n -> (someFunc y) n

# to get something that simplifies to
(someFunc y) x

view this post on Zulip Richard Feldman (May 05 2023 at 16:30):

oh interesting! I actually think x |> (someFunc y) should be a type mismatch

view this post on Zulip Richard Feldman (May 05 2023 at 16:30):

(I never really thought about it before)

view this post on Zulip Richard Feldman (May 05 2023 at 16:30):

but if I write (foo bar) that should just call foo applying bar regardless of what else is going on around it

view this post on Zulip Richard Feldman (May 05 2023 at 16:30):

expressions in parens should be self-contained

view this post on Zulip Richard Feldman (May 05 2023 at 16:31):

so I think the only way x |> (someFunc y) would work is if I'd defined someFunc = \arg1 -> \arg2 ->

view this post on Zulip Richard Feldman (May 05 2023 at 16:31):

that is, I'd manually curried it

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:31):

Yeah, I think that's what @Brendan Hansknecht is talking about

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:32):

See: https://roc.zulipchat.com/#narrow/stream/316715-contributing/topic/Record.20Builder.20Syntax/near/355297908

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:33):

We're talking about different things here, though. #6 can work fine because (manullyCurriedFn 31) 11 already works.

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:37):

The downside of not supporting x |> (someFunc y) is that someone cannot just use the apply functions that they'd use in a Record Builder to construct an applicative manually

view this post on Zulip Richard Feldman (May 05 2023 at 16:38):

hm, not supporting it in what sense?

view this post on Zulip Richard Feldman (May 05 2023 at 16:38):

what I was suggesting earlier is that x |> (twoArgFunction y) would not work

view this post on Zulip Richard Feldman (May 05 2023 at 16:39):

because (twoArgFunction y) would call the 2-arg function immediately, and that would be a type mismatch because it's only getting one argument

view this post on Zulip Richard Feldman (May 05 2023 at 16:39):

whereas x |> twoArgFunction y would of course continue to work

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:41):

Right. However, the desugared examples in your proposal make it look like you could either use Record Builder or pipe these curried functions. That's not currently possible. It'd be possible the way Elm does pipelines, but not in Roc.

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:43):

In my current implementation, I'm not using pipes. I'm just desugaring to Expr::Apply like this

view this post on Zulip Richard Feldman (May 05 2023 at 16:48):

ah, right - because the original proposal used collect rather than succeed

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:49):

I might be missing something, but I don't think Task.collect would've changed that. This is about Task.batch being curried.

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:51):

Agus Zubiaga said:

I think we overlooked something:

Task.succeed {
    users <- Http.get "/users" |> Task.batch,
    posts <- Http.get "/posts" |> Task.batch,
}

could not desugar to:

Task.succeed (\users -> \posts -> { users, posts })
|> Task.batch (Http.get "/users")
|> Task.batch (Http.get "/posts")

because we would be applying Task.batch with 2 arguments: Task.succeed (\users -> \posts -> { users, posts }) and (Http.get "/users")

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:52):

In the Task.collect case, Task.batch would still be applied with the same 2 arguments.

view this post on Zulip Richard Feldman (May 05 2023 at 16:55):

ohh gotcha

view this post on Zulip Richard Feldman (May 05 2023 at 16:56):

yep, I missed that!

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:56):

That's why @Brendan Hansknecht suggested that x |> (manuallyCurried y) could desugar to (manuallyCurried y) x instead of (manuallyCurried x y) which is a "too many args" error.

view this post on Zulip Agus Zubiaga (May 05 2023 at 16:57):

That would allow you to write:

Task.succeed (\users -> \posts -> { users, posts })
|> (Task.batch (Http.get "/users"))
|> (Task.batch (Http.get "/posts"))

view this post on Zulip Richard Feldman (May 05 2023 at 16:59):

oh, I think we're saying the same thing then?

view this post on Zulip Richard Feldman (May 05 2023 at 17:00):

so if I have a function like curriedDivide = \numerator -> \denominator -> numerator / denominator

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:00):

Richard Feldman said:

so I think the only way x |> (someFunc y) would work is if I'd defined someFunc = \arg1 -> \arg2 ->

If you mean this, then yes.

view this post on Zulip Richard Feldman (May 05 2023 at 17:00):

yeah

view this post on Zulip Richard Feldman (May 05 2023 at 17:00):

I can write e.g. answer = (curriedDivide 6) 2 and end up with answer = 3

view this post on Zulip Brendan Hansknecht (May 05 2023 at 17:00):

Cool, them |> currently has a bug cause it doesn't work that way.

view this post on Zulip Richard Feldman (May 05 2023 at 17:00):

yeah I think it has a bug :big_smile:

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:02):

Ok, great. I think that makes sense intuitively. When I write x |> (someFunc y), |> should not be concerned with was is inside the parens.

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:04):

Fixing that would allow libraries to expose a single Task.batch-like function that could be used with Record Builders or that can be piped to manually construct something else.

view this post on Zulip Richard Feldman (May 05 2023 at 17:06):

yeah I think it's fine to have that function be curried manually, because it unavoidably has a highly unusual type signature no matter what

view this post on Zulip Richard Feldman (May 05 2023 at 17:07):

it's not like the first one of these is normal-looking :big_smile:

batch : Task a err, Task (a -> b) err -> Task b err
batch : Task a err -> Task (a -> b) err -> Task b err

view this post on Zulip Richard Feldman (May 05 2023 at 17:08):

:thinking: given how uncommon this is, I wonder if we should format it as:

batch : Task a err -> (Task (a -> b) err -> Task b err)

view this post on Zulip Richard Feldman (May 05 2023 at 17:08):

just to make it extra clear that it's a 1-arg function which returns a function

view this post on Zulip Richard Feldman (May 05 2023 at 17:09):

in a curried language you'd know that because you encounter it very early on in your learning, but in Roc this would be so rare it might be confusing to beginners what two ->s in the same type signature means

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:11):

That might already be the case. IIRC the current parser requires the parens actually.

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:13):

Yup:

── NOT END OF FILE ───────────────────────────────────────── src/Pg/Result.roc ─

I expected to reach the end of the file, but got stuck here:

135│  apply : Decode a err -> Decode (a -> b) err -> Decode b err
                                                  ^

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:13):

The error could probably be improved though

view this post on Zulip Richard Feldman (May 05 2023 at 17:13):

nice - what does the formatter do?

view this post on Zulip Richard Feldman (May 05 2023 at 17:13):

(hopefully it doesn't remove the parens!)

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:14):

Crashes without the parens, and doesn't remove them when they are present

view this post on Zulip Richard Feldman (May 05 2023 at 17:14):

cool!

view this post on Zulip Agus Zubiaga (May 05 2023 at 17:18):

Made an issue: https://github.com/roc-lang/roc/issues/5379

view this post on Zulip Johan Lövgren (May 05 2023 at 17:54):

What I remember from Haskell is that the function arrow is right-associative because of automatic currying. So it makes sense to me that the parentheses are necessary in Roc since it doesn’t automatically curry

view this post on Zulip Agus Zubiaga (May 09 2023 at 00:56):

I'm still thinking about ways to improve errors and such, but it's now in a pretty good state and I opened a draft PR: https://github.com/roc-lang/roc/pull/5389

view this post on Zulip Agus Zubiaga (May 09 2023 at 00:59):

I'm pretty new to Rust and the compiler source, so please let me know if I did anything horrible :smiley:

view this post on Zulip Agus Zubiaga (May 09 2023 at 01:13):

I'll explore adding a new CalledVia tomorrow. I think it could help me give better hints for type-errors.

view this post on Zulip Agus Zubiaga (May 10 2023 at 01:53):

Without adding any special code for Record Builders, mismatched record types produce pretty good errors already!
CleanShot-2023-05-09-at-22.52.372x.png

view this post on Zulip Agus Zubiaga (May 10 2023 at 01:54):

These are actually coming from the generated closure, but I don't think they need any special handling

view this post on Zulip Agus Zubiaga (May 10 2023 at 01:56):

The following is one we could improve I think:
CleanShot-2023-05-09-at-23.03.102x.png

view this post on Zulip Agus Zubiaga (May 10 2023 at 01:57):

In that case, I forgot the |> apply at the end

view this post on Zulip Agus Zubiaga (May 10 2023 at 01:58):

Also, this:
CleanShot-2023-05-09-at-22.58.232x.png

view this post on Zulip Agus Zubiaga (May 10 2023 at 01:59):

It'd be great to suggest replacing the <- with a : in that case.

view this post on Zulip Agus Zubiaga (May 10 2023 at 02:01):

I'm going to start with that last one

view this post on Zulip Agus Zubiaga (May 10 2023 at 02:18):

CleanShot-2023-05-09-at-23.18.332x.png

view this post on Zulip Agus Zubiaga (May 10 2023 at 02:21):

Wording might have to improve, but I think that's helpful

view this post on Zulip Luke Boswell (May 10 2023 at 02:47):

This is very cool :sunglasses:

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:37):

In the following code the title field is missing a call to apply:

    succeed {
        id <- Pg.Result.i32 "film_id" |> apply,
        title <- Pg.Result.i32 "title",
    }

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:37):

This is how the error looks without any special logic for record builders:

CleanShot-2023-05-09-at-23.03.102x.png

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:39):

and this is what I have so far:
CleanShot-2023-05-10-at-00.39.062x.png

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:39):

which I think it's an improvement, but I'm not convinced

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:40):

It's tricky because we don't know which function they might need to call

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:41):

Also the wording seems awkward

view this post on Zulip Agus Zubiaga (May 10 2023 at 03:41):

I'm gonna go sleep on it!

view this post on Zulip Brendan Hansknecht (May 10 2023 at 03:52):

Yeah, definitely a weird error message both ways, but I don't have an immediate idea how to make it better.

view this post on Zulip Agus Zubiaga (May 14 2023 at 00:17):

This week has been super busy for me, but I finally got some time today to rewrite the parser and improve the errors. I think I'm ready to submit this for review!

view this post on Zulip Agus Zubiaga (May 14 2023 at 00:37):

https://roc.zulipchat.com/#narrow/stream/316715-contributing/topic/Pull.20Request.20for.20Review/near/358182443

view this post on Zulip Luke Boswell (Aug 14 2024 at 06:46):

I've been looking at https://github.com/roc-lang/roc/pull/6995 which adds a tutorial section on the new Record Builders, thank you @Sam Mohr

I'm thinking as this is in Advanced Concept section and it should provide only a minimal syntax sugar. Enough that someone who already familiar with the advanced concepts but not necessarily roc syntax would be ok with. We can generally wave our hands about what it is but no try to explain it; instead we can point the reader to a more detailed guide which walks it through from start to finish and builds up to the full feature.

I'm finding I have to really think about things like what it is, how it works, why I would use it, etc. I think a well designed API using record builders will be intuitive for people to use, at least the Sqlite one is very easy to work with as long as I can see a worked example. So I think a very surface level knowledge, essentially "what is this syntax called, this is where I can find more information, and don't sweat the details it's an advanced concept", is ok for a tutorial.

view this post on Zulip Luke Boswell (Aug 14 2024 at 06:47):

I'm interested to know what others think. It's not easy to write good content and I appreciate the effort that Sam has put into the PR.

view this post on Zulip Luke Boswell (Aug 14 2024 at 06:49):

To clarify -- I'm wanting to clarify the intent for the tutorial section.

view this post on Zulip Luke Boswell (Aug 14 2024 at 06:52):

So I guess I'm thinking; I should have a go at writing a guide for Record Builders to explore this further.


Last updated: Jul 05 2025 at 12:14 UTC