over the holiday break, I pretended a bunch of the design things we've been talking about already existed and built out this demo app (based on the realworld
spec) to see what the code would look like.
I didn't end up completely finishing it, but I learned a lot from trying all these things out, and I've exceeded my timebox...so I'm sharing where I ended up with it! :big_smile:
I made a little screen recording talking through the code, which ended up taking a lot longer than I was anticipating...but here it is:
https://drive.google.com/file/d/17fKjW91tb-rtkxv3xGTfUhvfWAuuNqth/view?usp=sharing
happy to talk about any of this, although I'm going to bed now, so may be awhile!
Looks interesting! Given you showed the time module, as a reference also https://effect.website/docs/data-types/duration/, https://effect-ts.github.io/effect/effect/Duration.ts.html#duration-overview and https://effect-ts.github.io/effect/effect/DateTime.ts.html.
It's sometimes really convenient to being able to simply use "3 hours" or the like and then being able to work with whatever unit you want at the place utilizing it.
Very cool! I was already going through your repos days before hoping to find it there already :big_smile:
Thank you for taking the time to explain these ideas. I found it really helpful to catch up on the future design direction for roc. Overall it's looking very exciting :smiley:
I've got some random thoughts while watching the video...
Looks like a nice pattern storing effectful functions in a Custom Record. You mentioned this replaced Module Params... the thought was "do we still need Module Params"? I'm interested to try and update my AoC template package with a similar pattern using an Opaque Type and an effectul function, and see if that works today.
How is gen
implemented? It looks a little like zig's comptime
feature. I assume these are builtins somehow and not something that can be written oustide the compiler?
I don't love the zero argument functions... I think I prefer making it explicit like it currently is and requiring the unit {}
arg. It's much easier to read at a glance and see it's a function. At the call site, can we have Time.now!()
be sugar for Time.now!({})
?
Re Instant
... one of the things I was debating in basic-cli was what size to make the nanos since epoch, rust uses a u128
doc.rust-lang.org/nightly/std/time/struct.Duration.html. I went with I128
to simplify maths using these... but that's probably less of an issue now with static dispatch. Just making an observation that you chose U64
in this video. It's probably nothing significant, but just wondering if there was much thought in that design?
Static dispatch looks so much more concise and less viausl noise :chef's kiss:
My thoughts on integration tests is to duplicate main.roc
and having a test.roc
, which means I can use different dependencies if I want etc.
That was a great watch! Loved how the code looked, a couple of things that stood out to me:
request => route
logic behind an abstraction, and then needing a lot of additional API to configure that abstraction (middelwares, hooks, ...).For testing against a real database, I think the main difficulty I've encountered writing tests against real external services in (non-Roc) work is the amount of effort required to setup a test harness for these types of tests with some nice features:
It'd be amazing if some part of the ecosystem took responsibility for providing a harness for the external service out of the box. I guess an opionated platform could, if it for instance only worked with a certain selection for databases and added built-in support for these. I guess Rails pushes in that direction a bit.
In the case of a platform-agnostic library though, say a Postgres client, I guess it'd make most sense for the library author to also provide test helpers, specifically in this case a test harness for writing tests against a real Postgresql database.
For a Postgres library, here's some random feature ideas for a test harness:
I'm not super certain about any of these ideas, the broader point I want to make is that the test harness might require very different and more comprehensive effects than the database client itself. In part such a test harness might require effects that the platform does not provide because it'd be bad idea in a production environment. For instance, a web platform might not want to provide any file-system access, but an ideal postgres harness might make use of the filesystem.
Not sure what the answer is. One thought is that maybe get!
and set!
are the tip of the iceberg, and that we'd potentially have additional test-only APIs. Creating an in-memory and sandboxed test directory for instance seems like something that could be usable in multiple scenarios, Zig offers a test helper for this which I make frequent use of.
I really love how the abstraction of effectful functions and packages not being able to rely on platform implementations is that we are basically created packages that are capabilities secure by design. And also possible to create easily testable effects!
I think depending on where we resolve static dispatch, we can have static dispatch method tear-offs. Especially easy if we do it in can
.
Tear-offs would allow you to write the Ok(router.handle_req!)
But this is a form of "partial application"
If router
's type is a custom type with a handle_req! method, you can desugar this to a closure that takes the rest of the args of the handle_req! method and then applies them to the method in static dispatch form (or a plain method if that desugaring has already happened).
I still think that the Str
builtin should have ascii_*
namespaced functions for capitalizing strings that are understood to be written using only ASCII representation. But we should also have a strong unicode package as well with great localization support.
Anthony Bullard said:
I think depending on where we resolve static dispatch, we can have static dispatch method tear-offs. Especially easy if we do it in
can
.
Tear-offs would allow you to write theOk(router.handle_req!)
I'm interested to explore this general topic - want to start an #ideas thread about it?
@Richard Feldman I would but I don't have the privileges to move to another channel
oh I mean just start one from scratch
Ah, OK
Luke Boswell said:
- How is
gen
implemented? It looks a little like zig'scomptime
feature. I assume these are builtins somehow and not something that can be written oustide the compiler?
yeah I was thinking we'd have some hardcoded list of supported ones in the compiler, just like we do today with Abilities
- I don't love the zero argument functions... I think I prefer making it explicit like it currently is and requiring the unit
{}
arg. It's much easier to read at a glance and see it's a function. At the call site, can we haveTime.now!()
be sugar forTime.now!({})
?
want to start an #ideas thread about zero-arg function syntax? I'd like to explore ideas!
- Re
Instant
... one of the things I was debating in basic-cli was what size to make the nanos since epoch, rust uses au128
doc.rust-lang.org/nightly/std/time/struct.Duration.html. I went withI128
to simplify maths using these... but that's probably less of an issue now with static dispatch. Just making an observation that you choseU64
in this video. It's probably nothing significant, but just wondering if there was much thought in that design?
it's a great question...also worth its own thread I think! :smiley:
Here's the thread: #ideas > Static Dispatch and method tear-offs
One last thing I'd like to say is: I may be in the minority (and I don't think this would be a "business move"), but I think videos like this would be interesting on a "Roc" YouTube channel, as well as the virtual meetups and such.
This looks great! I really like the discovery that static dispatch can also replace module params. People are very familiar with this pattern from other languages—the difference is just that all packages would have to use this approach for any I/O which gives us all the nice ecosystem properties we want. Also a nice saving for the weirdness budget :smiley:
Great video and I think that this dummy
realworld example turned out great!
I've had some trouble understanding expect |get!, set!|
part (1:46:25), was there some sort of proposal/discussion on zulip that I could dive into to get that better? I don't understand what set! is doing if we're passing manually created effects into Route.new
anyway.
EDIT. nvm, I've rewatched the video second time and I think I get it. Packages would create dummy clients, so we could use set! in our expect and package could use get! to get value back.
Re roc-pg connect function, I was actually already exploring making the stream opaque in the connect
function sometime ago. All it needs is a way to read and write from a stream. This would enable the package to be used with Unix Domain sockets or mocked/in-memory servers like Jasper suggested.
Just about a quarter of the way though. Very interesting so far! I was really surprised by how much I liked the logging API. I didn't think such a minimal design could still be so concise in user land
One question about the gen from_str_case_insensitive
part. (I am there now, so sorry if it gets answered later)
What about accents? (diacritics? Idk the difference). I guess in Turkish all the i's might be the same, but in many languages, there are words where putting an accent somewhere changes the meaning.
After a search online, even swapping Turkish i's changes meaning of words. (As well as slightly different pronunciation? I'm not Turkish so not sure. But they are different letters altogether)
interesting! Maybe this is a case where from_ascii_case_insensitive
would make more sense
assuming we add ASCII case-related things to Str
like Rust has
but keep full Unicode casing out of scope (unlike Rust)
I think that makes more sense for an autoderived function working on enum variants that can only be ASCII
Also, only supporting ASCII is probably better for the perf of the generated function
based on the roc-realworld repo, do we plan to remove try
@Richard Feldman ?
Part of me wishes we could keep try
for function level early returns on errors, and recover ?
as expression level recovery from errors
But that's my Javascript/Typescript/Dart brain probably
I think we should get rid of it to always prefer having one way to do things, unless we really need it.
But I see its value
Have we discussed what I just said above before?
Basically have ? postfix be a flatmap of the right side over the left side. I'm thinking of when we have SD
We had a big ol' discussion about it
Ok, I'll look for it
#ideas > `try` keyword instead of `?` suffix being one of them
The outcome is that it's really hard to make expression level work
In a way that doesn't have a lot of confusing rules
But "? returns errors from the function" is brain-dead
I will say, expression-level try
actually is pretty simple with methods and Result.try
:
decoded = Http.get!("api.com").try(.decode())
yeah I think going back to try
being a function in the Result
module is the way to go
This was a great watch.
I'm very excited for destructuring and field access for custom records, It's going to make decoding and encoding data super smooth.
I'm certainly sad to see Module qualified function calls go, I do agree they are clearer to read, but overall I definitely agree that the advantages of static dispatch outweigh the loss.
I haven't watched the whole codebase walkthrough yet, but on the topic of string interpolation, I think C# could be a nice inspiration: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated
They solved the "keep raw JSON in string literal" problem by... just adding more "
around the string literal. You indicate that the string is interpolated by doing $"some string {someVariable}"
- and you can add more $
in the beginning to indicate how many { you for interpolation.
That means that the following is possible:
var json = $$"""
{
"age": 5,
"name": {{userName}}
}
""";
It also takes care of the leading whitespace - it's really AWESOME. You can also just have """"
(four) around the literal, so that you can use """
inside of your string.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/improved-interpolated-strings#the-handler-pattern - there is also a way to hook into the interpolated string and for example use interpolated string for SQL commands with proper parameter handling, or for structured logging.
I think rust also does the same thing, which imo is very flexible and easy to understand. Just make sure that you put more surrounding quotes than there are in a string and you're good to go.
I just finished the whole things and wow - I can't wait for Roc to be done :)
as for tests, I again think that C# has this figured out :) with nUnit you can decorate methods with [Setup]
and [TearDown]
attributes, they will be run before and after each test in your test class (yes, you need a separate class for tests, a separate project to be fully honest).
There is also [OneTimeSetup]
and [OneTimeTearDown]
, and the coolest thing is that they are discoverable upwards. The discovery starts at the test class in a namespace MyApp.BlogApi.Tests
, then looks in MyApp.BlogApi
, then in MyApp
, then outside of all namespaces. They are also corresponding to directory structure (usually :)). All the setups (both one time and normal) are run from top to bottom, and teardowns from bottom to top.
So for example, you can have top-level class that starts TestContainer with PostgreSQL in [OneTimeSetup]
, and saves the connection string in a static variable. In the same class you can have [Setup]
that takes a snapshot of the db, and [TearDown]
that restores it.
Then at any level you spin up your api app using WebApplicationFactory, which basically runs your whole app. You get an HttpClient to it.
Cool thing is, you can even pass that client to Playwright runner, so you can have UI tests running on in-memory app! Even better, when you debug the test, you can debug both the API and the UI tests, both written in C#.
When I did AoC in Rust, I remebered reading that "Rust has testing built-in!". Ok, how do I run the same setup for for 5 tests? Ok, I just write a function that returns everything I need. But how do I run it once? Oh, there is a package that can run a thing once...
This doesn't feel like "proper" testing.
Another thing C# is doing better than others (IMO) is ORM for database access. EF Core is just awesome.
Main feature that powers it (and many other cool things) is Expression - basically a way to introspect code at runtime.
We all know the cool functional style making collections easy:
var myProductIds = products.Where(x => x.Product.Owner.Id == currentUser.Id).Select(x => x.Id).ToList();
The best thing about EF Core is that you can just use the same thing, but against the db:
var myProductIds =
dbContext // that's the whole db
.Products // DbSet<Product>, represents a table
.Where(x => x.Product.Owner.Id == currentUser.Id) // filter
.Select(x => x.Id) // project
.ToList()
(you can also put the query in a separate method and mark it with attribute to become a prepared SQL statement)
the above will generate
select Id from Products where OwnerId = ($1)
How does it work? Where
and Select
(and every other method like this, Count
, ToDictionary
etc) doesn't take Func<Product>
, it takes Expression<Func<Product>>
. What's the difference? Expression "knows" the code.
It's a bit complicated, but you can easily parse the function you passed into there and go "oh, we are comparing the Id property to something", and just spit out proper SQL.
It build's a description in IQueryable
- basically like a fancy iterator, that converts the expressions into a request to underlying tech, and enumerates the results. It's just awesome - no new syntax, you get compile-time errors if the expression cannot be converted to SQL (rarely comes up in reality).
I am not proposing that you actually write an ORM, but if the language had something like Expression<Func>
- all of this would be possible also in Roc.
Last updated: Jul 06 2025 at 12:14 UTC