Stream: bugs

Topic: Compiler panic for naming mismatch between type def and use


view this post on Zulip Ian McLerran (Jan 07 2025 at 17:15):

Not sure if this is a known bug, but a mismatch between the naming of a record field in its type annotation and its definition / usage will produce a compiler panic:

IE:

month_days : { month : Int *, isLeap ? Bool } -> U64
month_days = \{ month, is_leap ? Bool.false } ->
    when month is
        1 | 3 | 5 | 7 | 8 | 10 | 12 -> 31
        4 | 6 | 9 | 11 -> 30
        2 if is_leap -> 29
        2 -> 28
        _ -> 0

produces:

thread 'main' panicked at crates/compiler/gen_llvm/src/llvm/build.rs:5582:19:
Error in alias analysis: error in module ModName("UserApp"), function definition FuncName("\x12\x00\x00\x00\r\x00\x00\x00r,\xe4idl\x95\x92"), definition of value binding ValueId(3): tuple field index 2 out of range
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

view this post on Zulip Anton (Jan 07 2025 at 17:31):

I haven't seen that cause an error like this before, can you make an issue?

view this post on Zulip Ian McLerran (Jan 07 2025 at 17:33):

Yeah... although in trying to find a min repro, its a little more complex. Trying to identify the exact case that produces this in a min repro

view this post on Zulip Ian McLerran (Jan 07 2025 at 18:08):

Okay, this one is actually VERY specific, but I think I've narrowed down to a fairly minimum repro.

(Wasn't quite as specific as I thought for a minute...)

view this post on Zulip Ian McLerran (Jan 07 2025 at 18:16):

Conditions I have been able to establish:

Min repro:

main! = \_ ->
    _ = foo { bar: 1, baz: 1 }
    Ok {}

foo : { qux ? U16, baz : U8 } -> U8
foo = \{ bar ? 0, baz } ->
    if bar == 0 then
        0
    else
        baz

view this post on Zulip Ian McLerran (Jan 07 2025 at 18:18):

Note that I also tried reversing the U16 and U8 types so the optional is U8 and the non-optional is U16, but this does not trigger the error, so it does not appear to be based on the total size of the record.

view this post on Zulip Ian McLerran (Jan 07 2025 at 18:29):

Filed an issue @ #7478

view this post on Zulip Anthony Bullard (Jan 07 2025 at 20:43):

Gotta be something weird with alignment and layout

view this post on Zulip Brendan Hansknecht (Jan 07 2025 at 21:34):

In general, I think optional record fields are more a sharp edge than valuable in current roc.

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:41):

That feels more like a current problem with a fragile implementation than the eventual state of the feature, right?

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:41):

Or do you kinda feel like we should get rid of them

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:41):

They're great for UI building

view this post on Zulip Anthony Bullard (Jan 07 2025 at 21:52):

I think optional fields are a _real difference_ in Roc as a feature

view this post on Zulip Anthony Bullard (Jan 07 2025 at 21:52):

For UI specifically, the amount of noise you can avoid is insanely large

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:55):

It makes Weaver much cleaner

view this post on Zulip Richard Feldman (Jan 07 2025 at 21:56):

Brendan Hansknecht said:

In general, I think optional record fields are more a sharp edge than valuable in current roc.

:thinking: I wonder if static dispatch could make "builder" APIs comparably nice for specifying config

view this post on Zulip Anthony Bullard (Jan 07 2025 at 21:57):

If I ever get around to wrapping Clay in Roc, it allows for really nice API (like it has in C, but without dealing with the C preprocessor)

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:57):

Ooh :galaxy_brain: it would definitely work for Weaver

view this post on Zulip Anthony Bullard (Jan 07 2025 at 21:57):

There is an inherent cost to builder APIs, especially for things like immediate mode rendering systems

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:58):

Which would be a "zero-cost abstraction" with pure expression compile-time eval

view this post on Zulip Anthony Bullard (Jan 07 2025 at 21:58):

You assume that most of this configuration is static

view this post on Zulip Anthony Bullard (Jan 07 2025 at 21:58):

And that is not the truth a lot of the time

view this post on Zulip Richard Feldman (Jan 07 2025 at 21:59):

button(Config.default().border(green).enabled(foo))

I know we haven't decided for sure whether we want to do this, but you could eliminate the default config by taking a function that operates on it:

button(.border(green).enabled(foo))

view this post on Zulip Sam Mohr (Jan 07 2025 at 21:59):

Yeah, we'd need LLVM to pull in clutch 100% of the time on top of constant folding

view this post on Zulip Richard Feldman (Jan 07 2025 at 21:59):

if the config isn't static then we're doing runtime work anyway, probably makes no difference

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:00):

also I wouldn't be surprised if llvm would already optimize this away

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:00):

I think the real question is how it reads

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:00):

especially if you have a lot of them in a DSL :big_smile:

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:01):

Richard Feldman said:

I know we haven't decided for sure whether we want to do this, but you could eliminate the default config by taking a function that operates on it:

button(.border(green).enabled(foo))

I actually really like how that reads!

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:04):

it's about as concise as default record fields, but without the separate language feature

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:04):

I wonder how we can make a default button nice without needing a separate default_button(), since you need to do button(|c| c) here

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:05):

You could make . the identity function

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:06):

button(.)

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:07):

I'm telling you if we get rid of optional fields, with PNC we will absolutely end up with labelled/named arguments

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:07):

I don't have perms to move everything from #bugs > Compiler panic for naming mismatch between type def and use @ 💬 on into a different channel. Can someone do that?

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:07):

Might as well start bikeshedding the syntax now

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:07):

I think labelled args are great

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:07):

If you don't use records

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:08):

I worked on the Flutter team at Google - I like labelled args :-)

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:08):

But we have records already

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:08):

BRO

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:08):

So jealous

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:08):

I wasn't on the Glamorous side

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:08):

So not jealous

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:09):

I was _mostly_ diving into the Dart compiler and internal tooling/infra

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:10):

Anthony Bullard said:

I'm telling you if we get rid of optional fields, with PNC we will absolutely end up with labelled/named arguments

labeled arguments are a different thing from optional arguments. We already have records serving the purpose of labeling!

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:12):

Sam Mohr said:

button(.)

This aligns with the .method() syntax for desugaring to |x| x.method(), so . goes to |x| x. Literally trimmed off the end.

If we do something like _.method() goes to |x| x.method() instead, then maybe _. or just _ can be the identity function

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:12):

True-ish. But in a language like dart optional args are almost always labelled.

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:13):

Sam Mohr said:

button(.)

What if the function arg was....optional?

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:14):

We've come full circle

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:14):

Richard Feldman said:

Anthony Bullard said:

I'm telling you if we get rid of optional fields, with PNC we will absolutely end up with labelled/named arguments

labeled arguments are a different thing from optional arguments. We already have records serving the purpose of labeling!

I think there are a lot of reasons for labelling args. One can be the Swift/ObjC/SmallTalk approach where the set of labelled args are part of the function "name". One can be for clarity at the call site. And another is to allow arguments to be reordered and left out at the call site based on usage.

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:16):

And to be clear - I'm not saying labelled arguments are right for Roc.

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:25):

Anthony Bullard said:

Sam Mohr said:

button(.)

What if the function arg was....optional?

optional args are a deep topic. I'm happy to talk about it but I've thought about it a lot over the years and I keep coming back to the conclusion that they sound like a good idea but actually just lead to both worse APIs and also a worse experience for API designers

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:26):

I think that's true - but I think either optional args or optional record fields are needed in the language

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:26):

Or most UI libraries will be very verbose

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:26):

But I could be wrong

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:26):

I'm not sure what use case they are needed for that terse builders can't handle

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:26):

I know flutter couldn't work (in it's current design) without them

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:26):

Okay, we can write up an experiment

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:27):

Sam Mohr said:

terse builders

This sounds like magic

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:28):

What Richard is describing is what I'm referring to

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:28):

It seems almost as terse as optional record fields

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:28):

But we _could_ maybe find a way to have a UI library that has functions for elements that all return a root custom type that has all the config functions defined in the module, and then you can config that way

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:28):

Like SwiftUI

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:29):

button("Some text")
    .border(Colors.green)
    .enabled(foo)

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:30):

So the "main business" of an element/component happens in it's args. Anything that would wrap it or change some config of the root type would be a SD function

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:30):

Hmmmm, that works

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:30):

great point!

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:31):

I know Flutter has talked about experimenting with a similar design (to compete with SwiftUI I imagine)

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:31):

Anyway, it would be fun to experiment with this for my hypothetical Clay wrapper

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:32):

Someone with perms should really clean up this thread :-)

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:32):

Sorry @Ian McLerran !

view this post on Zulip Richard Feldman (Jan 07 2025 at 22:32):

@Sam Mohr I'd be curious what weaver would look like with this config idea

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:33):

I'll draft something later today, probably tonight

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:34):

Weaver is a great candidate, actually! It could help clean up the number of functions

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:35):

We don't need Opt.str({ short: "v" }), we can do opt(.str().short("v")) or maybe opt().str().short("v")

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:35):

I'll mess with it

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:35):

Currently we have 3 times 13 num functions for Opt and Param each

view this post on Zulip Luke Boswell (Jan 07 2025 at 22:45):

optional args are a deep topic. I'm happy to talk about it but I've thought about it a lot over the years and I keep coming back to the conclusion that they sound like a good idea but actually just lead to both worse APIs and also a worse experience for API designers

I'm using the optional args extensively in https://github.com/lukewilliamboswell/plume

I tried a few different API's, but this just felt the nicest. I'm very interested to know if they is another way!

view this post on Zulip Anthony Bullard (Jan 07 2025 at 22:51):

@Luke Boswell If the Trace type was a Custom Type and we had static dispatch you could just add a with_orientation(or just orientation) function to that module and initialize one with

Trace.new(data)
    .orientation(Horizontal)
    .name("This is my trace!")
    .bar_width(35.5)

And if you don't want to use any of that other stuff it's just

Trace.new(data)

view this post on Zulip Luke Boswell (Jan 07 2025 at 22:52):

Ohk... yeah they're all custom types. So we can try this out when we have static dispatch.

view this post on Zulip Sam Mohr (Jan 07 2025 at 22:52):

I'll make a branch of Weaver anyway just to show what the API would look like, but we won't be able to test it, per se

view this post on Zulip Richard Feldman (Jan 07 2025 at 23:13):

yeah, but it's really useful to know if this looks viable

view this post on Zulip Sam Mohr (Jan 07 2025 at 23:14):

Agreed

view this post on Zulip Richard Feldman (Jan 07 2025 at 23:14):

would be a very nice simplification to have static dispatch replacing Abilities, module params, and optional record fields :smiley:

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 00:43):

Note, default record fields are by no means fundamentally broken. I would assume the fix isn't even that hard (I just don't know that part of the compiler). Fundamentally, we just need to generate more specializations based on if the default is used or not (that or we could generate it at the call sight. Just fill in the full record).

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 00:46):

I feel like default record fields are a lot simpler and require less API overhead, but builders do work. I find builders quite heavy for simple things like preparing an SQlite transaction for example. Default is deferred transactions, but you can explicitly specify other options.

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 00:47):

And for a new user I don't want them to think about transaction types at all. So they will just use the default and not even know what it means

view this post on Zulip Richard Feldman (Jan 08 2025 at 01:14):

I'd still like to explore what APIs would look like. It's not just about the implementation, and how the code looks, but also about whether the language primitive is worth it overall in comparison to how the code would look if we didn't have it (in the static dispatch world)

view this post on Zulip Richard Feldman (Jan 08 2025 at 01:14):

plus they have been a recurring source of misunderstanding about how they work

view this post on Zulip Richard Feldman (Jan 08 2025 at 01:22):

another way to say it is: if we already had APIs based around static dispatch, would we accept a proposal to introduce optional record arguments?

view this post on Zulip Karl (Jan 08 2025 at 01:25):

My issue with the chaining config function approach is that I usually want an immutable struct for stability across the codebase but it has to be mutable for the construction chain to work. I've seen a two struct workaround where one builds up the config and then whatever is finalizing copies the values over to an immutable struct but it's verbose. Is there a fix for this that I don't know about?

view this post on Zulip Luke Boswell (Jan 08 2025 at 01:27):

You could use record builders maybe to enforce that using types?

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 01:39):

Richard Feldman said:

plus they have been a recurring source of misunderstanding about how they work

I strongly feel most of this is due to naming, poor explanations, and bugs rather than anything innate to the feature. (For example we still regularly call them optional record fields when they are actually default value record fields)

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 01:39):

But I totally agree we should explore other features. I think builder style apis can be great. They just suck for anything simple.

view this post on Zulip Richard Feldman (Jan 08 2025 at 01:47):

Karl said:

My issue with the chaining config function approach is that I usually want an immutable struct for stability across the codebase but it has to be mutable for the construction chain to work. I've seen a two struct workaround where one builds up the config and then whatever is finalizing copies the values over to an immutable struct but it's verbose. Is there a fix for this that I don't know about?

in Roc they'd all be (semantically) immutable, e.g.

button("Done").border(green).enabled(is_enabled)

the types of these functions would be something like:

Elem.button : Str -> Elem

Elem.border : Elem, Color -> Elem

Elem.enabled : Elem, Bool -> Elem

view this post on Zulip Karl (Jan 08 2025 at 01:48):

But the Elem that's actually holding all the values would still be mutable, yes?

view this post on Zulip Karl (Jan 08 2025 at 01:49):

Or are we producing new immutable Elems that are partially initialized?

view this post on Zulip Richard Feldman (Jan 08 2025 at 01:53):

Roc doesn't have semantically mutable values :big_smile:

view this post on Zulip Richard Feldman (Jan 08 2025 at 01:54):

https://www.roc-lang.org/functional#opportunistic-mutation

view this post on Zulip Sam Mohr (Jan 08 2025 at 08:57):

Here's my experimental results for a Weaver API using terse builders: https://gist.github.com/smores56/dc7b37f73114df11d28cd6a148987dea#file-weaver-builders-roc

app [main!] { cli: platform "<basic-cli>" }

import weaver.Opt
import weaver.Cli
import cli.Stdout

main! = |args|
    data =
        cli_parser.parse_or_display_message(args)
        .on_err!(|message|
            Stdout.line!(message)?
            Err(Exit(1, "")))
        )?

    Stdout.line!(
        """
        Successfully parsed! Here's what I got:

        ${data.to_str()}
        """
    )?

    Ok({})

cli_parser =
    { Cli.weave <-
        force: Opt.flag(.short("f").help("Force the task to complete.")),
        alpha: Opt.single(
            .u64()
            .short("a")
            .help("Set the alpha level.")
            .default_fn(|| 1.left_shift_by(7)),
        ),
        files: Param.list(.str().name("files").help("The rest of the files.")),
    }
    .finish({
        name: "basic",
        version: "v0.0.1",
        authors: ["Some One <some.one@mail.com>"],
        description: "This is a basic example of what you can build with Weaver",
    })
    .assert_valid()

view this post on Zulip Sam Mohr (Jan 08 2025 at 08:59):

In the linked gist, I put most of the code we'd need to parse an Opt. This API just plugs into the existing Weaver codebase, so it's not much of a change for this to work

view this post on Zulip Sam Mohr (Jan 08 2025 at 08:59):

Some nice things:

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:00):

To parse a U64 arg, we can shorten the function

|arg|
    str = arg.as_str()?
    str.to_u64()

to the following with methods and Result.try

u64 : ArgTypeSelector -> ArgValueParser U64
u64 = |ArgTypeSelector.()|
    ArgValueParser.({ type_name: "u64", parser: .as_str().try(.to_u64()) })

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:02):

We can now ensure that either short or long must be called, whereas that's not possible to handle nicely in a single record with optional fields

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:02):

Since { short ?? Str, long ?? Str } has both as optional to allow either to be passed, but then both could be empty

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:04):

We can ensure short or long are empty by making the builder function passed to Opt.single(...) go through a few types:

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:05):

I can't really type check this code, but I'm 99% sure that all of this will work with the planned static dispatch and custom types behavior

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:07):

Anyway, there might be a slight aesthetic drop? But otherwise this seems like an improved API over optional record fields to me, since you get more opportunity for type safety with basically the same number of characters

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:08):

The one cost I don't know how to fix is field name punning, but I don't think syntax sugar is important for that

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:08):

Though it's something we could consider

view this post on Zulip Luke Boswell (Jan 08 2025 at 09:08):

Do you have to call things in the correct order?

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:08):

This API requires you to call in certain groups: type, then arg name, then everything else

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:09):

But you could definitely make it handle arbitrary order

view this post on Zulip Luke Boswell (Jan 08 2025 at 09:09):

Like

Opt.single(
    .u64()
    .short("a")
    ...

Versus

Opt.single(
    .short("a")
    .u64()
    ...

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:09):

I required ordering because I thought it would improve readability

view this post on Zulip Luke Boswell (Jan 08 2025 at 09:09):

I like it

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:09):

We could require the type at the end

view this post on Zulip Sam Mohr (Jan 08 2025 at 09:15):

A weakness of this API stemming from an intentional weakness of the API: I couldn't get just opt(.single().u64()) to work because we'd need to allow representing HKTs. This is now another place where HKTs are useful, but they're still not necessary for a good API

view this post on Zulip Anthony Bullard (Jan 08 2025 at 15:43):

How hard would this to be rework using the SD-style approach I laid out? Where the required args are in the main constructor, and all optionals are set in SD funcs?

view this post on Zulip Ian McLerran (Jan 08 2025 at 16:11):

Roc-ai is already doing something similar to this for client configuration, albeit without PNC or static dispatch, so not exactly the same…

client = Client.init { apiKey }
    |> Client.setUrl url
    |> Client.setModel model
    |> Client.setProviderOrder providers
    |> Client.setTemperature temp

The init function takes a record with 1 required field and 17 optional ones, but then there are 18 corresponding set functions. That makes for a very bloated signature for the init function. At least in such an extreme example (similar to weaver) I think chained function calls really is cleaner than optional record fields.

Additionally, the init function actually has to call calls the setter functions itself for 6 of the params, because the parameter accepted in as an argument is actually a different type (simplified) than what is actually stored, so optional record fields can’t handle this type conversion at all.

view this post on Zulip Anthony Bullard (Jan 08 2025 at 16:16):

So that could be with PNC/SD:

client =
    Client.init(apiKey)
        .setUrl(url)
        .setModel(model)
        .setProviderOrder(providers)
        .setTemperature(temp)

view this post on Zulip Anthony Bullard (Jan 08 2025 at 16:17):

The big advantage of optional record fields here is performance. In this case you would be avoiding four function calls

view this post on Zulip Ian McLerran (Jan 08 2025 at 16:19):

Yeah, if you have to create 18 stack frames with 18 copies of client, that’s a lot less optimal than just one…

view this post on Zulip Anthony Bullard (Jan 08 2025 at 16:20):

It depends on inlining

view this post on Zulip Anthony Bullard (Jan 08 2025 at 16:20):

Which I have no idea if/when LLVM would inline these

view this post on Zulip Anthony Bullard (Jan 08 2025 at 16:21):

If they are reliably inlined, there is probably no perf difference with --optimize

view this post on Zulip Ian McLerran (Jan 08 2025 at 17:07):

Richard Feldman said:

would be a very nice simplification to have static dispatch replacing Abilities, module params, and optional record fields :smiley:

Wait how will/could SD replace module params? Zulip link anyone? :fingers_crossed:

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 17:17):

Anthony Bullard said:

If they are reliably inlined, there is probably no perf difference with --optimize

As much as llvm inlines everything, I would expect this to still manifest some copies (especially with more complex types). At the same time, I would be quite surprised if that generally affects perf.

view this post on Zulip Brendan Hansknecht (Jan 08 2025 at 17:17):

As a simple example, refcounting between copies likely forces llvm to copy.

view this post on Zulip Sam Mohr (Jan 08 2025 at 18:53):

@Ian McLerran check out Richard's recent Realworld application impl. Having tried to use module params vs. just a record that saves the relevant effectful closures, it seems like the latter with SD is simpler and has fewer sharp corners like testing and all. #show and tell > roc-realworld initial exploration @ 💬


Last updated: Jul 06 2025 at 12:14 UTC