@Karl @Romain Lepert
We were discussing the general friction around extending platforms with the record "bundle of effects" pattern (as opposed to specifying where constraints on everything).
I've refactored roc-ray to follow this pattern in this spike https://github.com/lukewilliamboswell/roc-ray/pull/186, and also included a downstream package authoring guide here https://github.com/lukewilliamboswell/roc-ray/blob/3fe44434388204756b28e1603dceb6943f59b06c/docs/package-authors.md
It's only a minor change really from an app authors perspective, but I think it completely unlocks downstream package authors.
I would appreciate any feedback etc around this... I feel like we give it a try in the 0.10.0 version of roc-ray and if packages like Romain's terrocotta are still nice to build, then we consider doing something similar for basic-cli and basic-webserver etc.
The goal would be to really reduce the coupling of those cli and webserver to a particular higher level API and reduce the boundary to the minimal or lowest level interface, so that downstream packages across the ecosystem can build on and innovate from that foundation.
2 messages were moved here from #announcements > roc-ray 0.9.0 by Luke Boswell.
I'll give some feedback this evening
:thinking: I don't understand why the app author experience would need to change
couldn't the previous API be implemented in terms of this behind the scenes?
(I mean for using the platform API directly as opposed to using a package)
hm, I also don't see why accepting Drawing.Effects is better than accepting a where with a method requirements alias :thinking:
the overall design principles that I think will lead to the best experience:
*-types dependency), and also ideally the platform does not need to change in any way; the package can say "I need these operations" and can specify all the relevant types itself (e.g. using where clauses when it needs nominal types) and all of that can be self-contained inside the package itself.honestly I think the *-types package is a trap and we shouldn't do it. It makes package code more concise, at the cost of:
*-types package*-types package, and no longer have the option of deviating in even small ways that could make more sense without affecting package compatibility in practice - except that packages are coupled to this very rigid definition of what the types must exactly be crash "this is not actually used and should never happen" all over the place), which is an example of package authors choosing their own convenience (not having to specify only the effects they need as opposed to the broad "all the drawing effects" that's more concise for the package author) over the experience of app authors, which again I don't think is the tradeoff we should encourage. The whole point of packages is to improve app authors' lives! :smile:so to reiterate, what I really strongly think we should be doing is:
*-types packages an antipattern and never doing themwhere clauses defined in the package itself (even though that appears like unnecessary code duplication; I argue that this appearance is deceiving, and that the package itself is the best place for "what the package needs" to be precisely specified, not a separate dependency)if we try this and it turns out it there's some blocker that means it can't be done this way (I can't think of any but maybe there's one I'm not thinking of!) then we should address things like that on a case-by-case basis imo!
I also don't see why accepting
Drawing.Effectsis better than accepting awherewith a method requirements alias
I agree I don't see the particular benefit, whether for app authors or package authors. In particular because the example is Drawing.Effects which is what I'd like to get rid off to begin with :sweat_smile:. I would prefer that frame just carries the subset of Host.roc effects that are supported in the render! lifecycle phase. That is all that is needed. Then packages can do where clause on frame and build whatever API they want on top.
- considering
*-typespackages an antipattern and never doing them
In this case it is not an option, it is a requirement. roc-ray host can creates resources (Font, Texture, Shader, etc.) that are addressed through an opaque nominal handle.
Texture := { handle : Handle, width : F32, height : F32 }.{
Handle :: Box(U64)
}
Only effects can create them (e.g. load_texture! : Str -> Texture) so it is impossible for an app to manufacture a fake handle that does not have an associated hosted resource.
Packages can't match Texture structurally because of the nominal handle, and can't import Texture platform types. So package can't work with basic platform data structure. However if Texture lives in *-types then it can be imported, which means packages and platform can agree on Texture type. That is the reason *-types was introduced.
You say "[packages] can specify all the relevant types itself (e.g. using where clauses when it needs nominal types)". I don't understand how where helps here :thinking:
Now as to the other arguments against *-types package:
All that to say that i'll give it a shot nonetheless :laughing: , but it is not without friction and eyebrow raising moments.
Romain Lepert said:
- considering
*-typespackages an antipattern and never doing themIn this case it is not an option, it is a requirement.
roc-rayhost can creates resources (Font, Texture, Shader, etc.) that are addressed through an opaque nominal handle.Texture := { handle : Handle, width : F32, height : F32 }.{ Handle :: Box(U64) }Only effects can create them (e.g.
load_texture! : Str -> Texture) so it is impossible for an app to manufacture a fake handle that does not have an associated hosted resource.
Packages can't matchTexturestructurally because of the nominal handle, and can't importTextureplatform types. So package can't work with basic platform data structure. That is the reason*-typeswas introduced.You say "[packages] can specify all the relevant types itself (e.g. using
whereclauses when it needs nominal types)". I don't understand howwherehelps here :thinking:
ah! so my thinking here is that you use type variables to relate these - e.g.
where [
assets.load_texture! : Str => Try(texture, _),
draw.texture_at : texture, Vec2 -> texture_draw,
draw.texture! : Frame, texture_draw => {},
]
in other words, "we don't care specifically what a texture is (although if we need methods from it, we put where clauses on its texture type variable" aside from "it's the thing that load_texture! produces and texture_at receives"
does that make sense?
Romain Lepert said:
- if a package could import the platform types directly.
I think we should consider this ... it would solve a few problems. The platform is just a package like any other, it exposes types. Why couldn't a package import a platform package? there would be nothing it can do with the hosted effects, I would expect a compiler error if a package tried to use one of those.
This would eliminate the need for roc-ray/types package because then downstream packages like terracotta can see the definitions for Mouse Keys etc and reuse those.
Question: in this design, is it the platform or the package or the platform that's end-responsible for the app-authoring experience?
I'm wondering because I see sort of two different possible designs here:
My sense is these proposals are motivated by a desire to make the first model work better. Is that fair?
Yes. The design is around the first use case. Platform provides the host data types and effects. Package provides the app authoring experience (~framework).
Gotcha, thanks! I guess my follow-up question then is: for creating some nice app authoring experience, why choose to do a package over a platform? Platforms can do anything a package can, plus there's extra benefits:
Isn't that the nicer experience for the app author and the platform/package designer?
Richard Feldman said:
in other words, "we don't care specifically what a
textureis (although if we need methods from it, we putwhereclauses on itstexturetype variable" aside from "it's the thing thatload_texture!produces andtexture_atreceives"
Ok, that approach actually highlights the genericity complaint that I have.
Here is a PR that would make terrocotta generic over texture instead of using a concrete rrt.Texture.
Because a layout node might hold a texture (image node), then LayoutNode becomes LayoutNode(texture) and it bleeds to everything upstream of LayoutNode
LayoutNode(texture)
ElementOp(msg, texture)
Layout(texture)
View(msg, texture)
Program.State(model, msg, texture)
1) It is not great for the package authoring (see ./package diff).
There only place that make use of the texture: it is the frame.texture!(texture) call in Renderer.draw_image!(). This one legitimately could warrant the genericity, but the genericity bleeds all the way up.
2) It is not great for the app authoring (see ./examples diff).
These two types are user facing: Program.State(model, msg, texture), View(msg, texture)
texture concrete with Model : Program.State(AppModel, Msg, Assets.Texture)texture genericity even though they don't carry any texture. Like the README.md button examplebutton : Str, Msg -> View(Msg, texture)
button = |label, msg| {
box({ style: |status| style.width(Fit({})), events: [OnClick(Increment)] }, [
text(label),
])
}
This is the function/type coloring problem all over again, but this time for something that seems even more basic than async or lifetimes (both of which you actively designed to avoid :wink: ).
Now this is because of the Image(texture) node, but terrocotta also has Text(font) nodes and eventually wants to support Canvas(frame) nodes (charts, games, vector drawing editor, etc.) and Shader(shader)nodes (hue wheel, audio spectrogram, heatmap, etc.).
These will duplicate the same genericity bleed and user will end up with View(Msg, texture, font, shader, frame) and Program.State(model, msg, texture, font, shader, frame).
today i can avoid the texture, font and shader type parameters thanks to the *-types public package which gives the concrete types. However I can't avoid the frame generics because it is just a type carrying effects methods and *-types package can't describe that.
I see, and I agree - in practice that doesn't seem nice. Thanks for making that PR so we can see how it looks fleshed out!
Romain Lepert said:
Yes. The design is around the first use case. Platform provides the host data types and effects. Package provides the app authoring experience (~framework).
I think this is probably the root of the friction here: arguably the most unique thing that Roc does which no other language does is to be designed around the "platform is responsible for app authoring experience, and so building frameworks on top of platforms do not make sense" :smile:
so I think this supports the conclusion that the root of the problem here is trying to fit a (platform+package) shaped solution into a platform-shaped hole :smile:
concretely, I think the earlier direction we discussed briefly - of these layout primitives making more sense in roc-ray as opposed to in a separate package - was the right direction after all!
I was definitely under the impression that we thought platforms would trend towards lower level or more common abstractions and the packages would sit above that. This is how I have been framing this research.
oh definitely not, that's a miscommunication on my part then!
the vision for platforms has always been "the platform author is in charge of crafting a batteries-included app authoring experience for a particular domain" - just like Elm does
the whole platform concept came out of me trying to answer the question "how could I get a curated, Elm-like experience in a long tail of domains?"
So where does something like the whole bugsnag discussion fit in ... like making packages that are cross-platform?
those are things that aren't really domain-specific
like error logging services are a thing you can want when doing a GUI, a CLI, a server, a raspberry pi that controls a thermostat... :smile:
Ah ok ... so if e.g. terrocotta was simply the clay layout algorithm that would be cross-platform, but when it also includes the drawing and other things it becomes more coupled to the domain and the platform.
maybe another way to think of it: here are some types of GUI apps people build outside of Roc
each of these approaches could be a Roc platform
Ok, here's another angle ... what if I wanted to make a Calendar widget in a package for roc-ray? should I be able to do that, or how would I distribute that?
I guess in this case the type var would be less of an issue
yeah that's worth trying out concretely to see how it would look I think
It sounds like in this "new world order" terrocotta should instead be focussed on the core algorithms and data structures and then roc-ray would be a consumer of instead of a dependency.
At the risk of oversimplifying
![]()
Or another framing... all packages should probably be aimed at a cross-platform API, if it's coupled to a single platform then that is probably a smell
I'm just trying to make sense of things here... not claiming this is "the way" or anything. We've got a few directions to experiment in
yeah one way I've been thinking about it is "reducing layers"
like one of the reasons I chose the name "platform" instead of "framework" (which was a name I strongly considered at one point!) was that a Roc platform can in theory provide everything all the way down to - and including - the operating system.
So like a unikernel for a server, or a video game console game development platform.
and one of the reasons I think that's valuable is that I think we can attribute a good amount of software slowness to "it's fundamentally hard to make things faster when you have this many layers" because each layer can only optimize critical paths down to the layer right below it, and any optimizations that require going deeper than that are just blocked by the abstraction boundary
like for example, I'd question the premise that terroccotta should be a package as opposed to a name for a design
why not have that be the name of the design that's used inside roc-ray, and might also be used in other places, like The Elm Architecture is?
then none of the problems we've been discussing exist, plus I have to imagine it becomes possible to achieve better performance than what's possible if it's in a separate package :smiley:
I think most of terrocotta's algorithm is useful in a package and could be used by any platform.
I feel like maybe the Program part which wraps the app's model and messages is where things get a little blurry.
Like maybe the drawing in terrocotta would return a list of draw commands the app should then be responsible for translating into the platform's API -- as opposed to taking the effects and using those.
I don't quite understand how this would play out for the ecosystem. Won't this lead to anaemic Roc packages? If they can't take advantage of a platform then they have to get merged into the platform itself. This requires agreement from the existing devs/users of the platform, and so a fork is more likely instead. The outcome is a broad collection of deep platforms, each may be reusing many of the same ideas and code.
On the other hand, even if it becomes bad practice, packages may still be written for a specific platform but in a less efficient way. Almost the exact opposite in what is intended by reducing layers: packages will interface with their target platforms abstractly and with more intermediary glue.
@Jonathan what specific packages do you have in mind though?
my point here is not "all packages should be merged into platforms" but rather "layout primitives are so heavily intertwined with a platform's responsibility that it makes more sense for them to be integrated into the platform rather than separated as a different package"
and I think it's really important to be careful with how we evolve the ecosystem, especially in the early days - a lot of package ecosystems end up with really undesirable characteristics (e.g. normal npm packages having gigantic build graphs of hundreds of megabytes of microdependencies) because things were done for expedience early on, and they became cultural norms
so when we find a point of friction I think it's really important to look at the specifics there and not jump to generalizing that specific friction point to the entire future ecosystem :smile:
because it's predictable that if we talk through things on a case-by-case basis, each situation will turn out to have different tradeoffs
So far it has mostly been personal utilities, things that don't make sense to share because they are coupled to my specific projects and basic-cli. Nonetheless, I access the sqlite jobs/results db in different scripts for different purposes (loading in data, enqueuing jobs, subsequent analysis), and all these are having to be merged under one app so that I can reuse the helpers. That is fine but it's been a small pressure so far.
A bigger one would be some kind of query builder and SQlite api. I would like something somewhat like Ecto.Query from Elixir because I find it invaluable for running quick queries, and I imagine Roc would be able to make that more enjoyable with e.g. some applicative builder pattern. But to use it, it would either be coupled to the api of the platforms Sqlite module via extensive where clauses (I have been trying this and it basically meant shadowing the exact API of basic-cli.Sqlite, which fortunately uses aliases not nominal types), or I would have to provide the glue. I am trying out Roc over standard scripting languages because I love most of its ergonomics, but it seems like layering and abstractions are important here because the code is not for long-term maintenance and the upfront cost is not worth it.
oh interesting! I think a query builder should be able to be totally platform-agnostic, and honestly probably relational database agnostic too :smiley:
I.e., opinionated abstractions over platforms save me time, because my aim is not to produce faster software, but correct software, fast.
like for example, suppose you have this:
to_sql : Query -> (Str, List(List(U8)))
in other words, it turns the query you've been building into a string query with query params question marks in the right places, and then a list of the bytes of the actual query param values
or maybe alternatively you could offer to_sqlite and to_pg etc. because different databases use different binary representations for integers etc
or you could have it return (Str, List([Bool(Bool), Str(Str), I8(I8), U8(U8), ...etc]) and then hand that off to a specific database to convert into its format
But because this is abstract, it is already not taking advantage of basic-cli.Sqlite's parameter bindings, where indeed another platform's db interface might not offer bindings and just take raw strings.
I think query builder would be a good example where I don't think you'd want any effectful functions in the entire package, and it would be a natural fit for being platform-agnostic
but ok let's explore the hypothetical of "packages can depend on platforms"
so let's say you can make a package called like basic-cli-sqlite-query-builder
how would its API be different from one that didn't depend on basic-cli?
in other words, what specific types or functions from basic-cli would it make use of from that dependency?
I think one is that the decoders/encoders could be built automatically. If the query builder is aware of the output shapes and types and bindings, perhaps it could also construct the decoders to reduce the risk of me (happened already) trying to decode a Text column to an I64. This would require knowledge of the shape of function that is expected of a decoder, and I'm not sure how universal that is.
gotcha - but that's about SQLite, right? not necessarily basic-cli
as in, the encoding and decoding is going to/from the bytes that SQLite expects, and any time you'd be doing this with SQLite you'd have the same encoders and decoders even if it wasn't basic-cli specifically
Perhaps, but doesn't it also depend on the type signature of the decoder that basic-cli expects you to provide?
I mean, if it is, it does seem like it would be somewhat trivial in this specific case to write glue. Glue to connect the generic queries to the specific arguments that query_many! expects. Glue to connect the decoders, and also to wrap the variables in the tags basic-cli expects.
Even still, that is the same glue that I am writing for every script that connects the builder to basic-cli. Perhaps I am just too allergic to the idea of vendoring and copy-pasting code.
oh I don't think any glue would be needed in this situation
so in basic-cli, Sqlite.query_many! accepts a query Str and then a list of bindings, where the each binding is { name : Str, value: SqliteValue }
and then SqliteValue is:
SqliteValue : [
Null,
Real(F64),
Integer(I64),
String(Str),
Bytes(List(U8)),
]
so that's plain old structural tag union, meaning any package can produce that without needing to depend on basic-cli or anything else
so earlier I had this example:
to_sql : Query -> (Str, List(List(U8)))
and then I noted another way the query builder could go is:
Richard Feldman said:
or you could have it return
(Str, List([Bool(Bool), Str(Str), I8(I8), U8(U8), ...etc])and then hand that off to a specific database to convert into its format
so let's say the query builder package specifically offers this function:
to_sql : QueryBuilder -> {
query : Str,
bindings: List([
Null,
Real(F64),
Integer(I64),
String(Str),
Bytes(List(U8)),
]),
}
now in your application you can do this:
{ query, bindings } = my_query_builder.to_sql()
Sqlite.query_many!({ path, query, bindings, rows })
(I'm assuming in this example you already have path and rows defined, since those are unrelated to query-building)
this can be implemented in a zero-dependency query builder package that's just pure functions and data structures, no effects and no platform or package dependencies, and no glue code or vendoring needed either! :smiley:
does that make sense?
It does yeh, although it does still reflect the shape of sqlite as it is. I can imagine a different platform providing its own structured query type, or different nominal or structural value tag names (I realise this is incidentally lining up with basic-cli, vs explicitly depending on its types). But as for the decoder (rows), that was one of things I wanted to investigate being built by the query builder.
Jonathan said:
It does yeh, although it does still reflect the shape of sqlite as it is. I can imagine a different platform providing its own structured query type, or different nominal or structural value tag names (I realise this is incidentally lining up with basic-cli, vs explicitly depending on its types).
yeah, so I think that distinction is really important!
for example, let's say another platform uses a different representation for the bindings, like instead of Real(F64) and Integer(I64), they use F64(F64) and Int(I64)
this is where anyone can write a tiny function that translates between the two
or, as a convenience, the query builder package can offer multiple versions - e.g. to_sqlite() can return the SQLite format, and to_pg() can return the postgres format, etc.
but this "be shape-compatible rather than depending on the platform's nominal types" design has some nice characteristics:
F32 and so wants a tag for that, whereas another doesn't support it and so must not have a tag for that - but in cases where it is possible, a common shape can make things Just Work Jonathan said:
But as for the decoder (
rows), that was one of things I wanted to investigate being built by the query builder.
I haven't looked into this specifically, but in general parsing (aka decoding) packages should tend to work well as platform-agnostic packages, because they're providing pure functions that take arbitrary bytes (or strings) as inputs and then parse them, so they need to be aware of the format of those bytes but not any specific nominal types (from any platform or other package)
that said, looking briefly at Sqlite in basic-cli, it seems like it already provides all the functionality to make an auto-derived parser, so I think a nicer answer there might be to have it work more like how Json works
so that you wouldn't have to provide a parser/decoder at all, but rather just let the compiler infer one from how you're using the outputs of query_many! - which would mean it would be more ergonomic and there'd be no need for a package at all to do that :smile:
@Luke Boswell have you ever looked into that?
Yeh I was just looking into this, except the decoders are intertwined with the host effect of looking up the value, rather than being a decoder on some bytes directly.
gotcha, yeah I think the best way to improve that experience is to make it so that nobody needs to write those decoders by hand anymore :smiley:
but will see what Luke says when he's up - it's the middle of the night in Australia :flag_australia:
Say for example that Luke disagreed or at least was not yet sure, is this not a case for being able to depend on a platform's functions? So that people can experiment or abstract over what a platform provides? :laughing:
That is a hypothetical again, though :face_in_clouds:
this is mostly a case of "I can't think of a reason this wouldn't be better across the board, but maybe Luke already tried it and there's some blocker I'm unaware of"
Jonathan said:
So that people can experiment or abstract over what a platform provides
to be clear, you can already abstract over what a platform provides - we've discussed a few ways of doing it above - I think the main thing is that the ecosystem is in its infancy right now, so we don't have established precedents for "here is the right way to organize things around scenarios like this"
Yes that wasn't very precise - I meant particularly while using hosted functions and nominal types (and was feeling mischievous :laughing:)
for example, from discussing these 3 scenarios, my conclusion is:
Sqlite API infer the parser for you, so the app author ergonomics are better and there's no demand for a separate package in the first placeI also think that all 3 of those make things nicer for app authors:
so I think the best next step is to try out the above strategies and see how they feel in practice - if we don't like how they feel, we can always discuss having learned more! :smiley:
I haven’t had the time to read the whole discussion, mostly the first half so sorry if I argue against something already settled. IMO though it is idealistic/naive to think that a platform will be able to cover a full domain nicely. If the domain is small, sure. But for big projects, certainly not. Think game engines, even web platforms.
A platform authors for a game engine certainly cannot be an expert in arts + 3d modeling + rendering + specific console hardware + physics mechanics + artificial intelligence (and other machine learning extensions) etc.
A web platform author cannot support all of the web apis accross audio, bluetooth, crypto, video, streaming, gpu, etc, for the same reason. Elm is a perfect example. Evan abandoned the idea of increasing the web api support in Elm, so everything that you need to go through ports for, is in practice a terrible user experience. Not every web api fits well the "client/server" architecture of a port.
So IMO having flexibility in the platforms to be extended will just be a natural demand for all the things that require platform access whether for side effects, performance, security, etc.
oh I think game engines are a great example of what I have in mind as the scope!
like I'd say a game engine has a pretty clear story for "here is how you build a game in Unreal Engine" or "here is how you build a game in Unity"
and part of that story is where to draw the line on what primitives they provide, sure
but if you draw the line at "Unreal Engine provides C++ bindings to cross-platform rendering and audio primitives and that's it" then I wouldn't say it's really offering a story for how to build games - it's just taking some low-level C libraries and providing a C++ API on top of them
so maybe you don't draw the line at physics (for example) but you do provide an entity-component system as part of the batteries you're including
Matthieu Pizenberg said:
A web platform author cannot support all of the web apis accross audio, bluetooth, crypto, video, streaming, gpu, etc, for the same reason. Elm is a perfect example. Evan abandoned the idea of increasing the web api support in Elm, so everything that you need to go through ports for, is in practice a terrible user experience. Not every web api fits well the "client/server" architecture of a port.
I remember talking with Evan in person about this back when he was still working at Prezi, and the conclusion at the time was that it made the most sense to have first-class Elm APIs for all of them, while acknowledging that it would be a long road to get full coverage. So I think the reason Elm doesn't have full API coverage over the Web isn't that it's fundamentally unachievable, or a bad idea, but rather prioritization of other projects ahead of that one.
the scope of Roc platforms is much, much smaller than the scope of Elm (which also includes a compiler, package manager, now Acadia too, etc.), so I don't think we should assume a Roc platform targeting the web would hit the same problem.
I assume it's not controversial to say that the best app author experience is one where all the APIs they want to use on the Web are available as first-class Roc APIs, as opposed to "these things are forever your responsibility to bind to using JavaScript" or similar. You could look at that and say "yes, which is why people in the community should be able to go off and build third-party Roc bindings to the Web APIs and publish them as separate packages," but I think it's better for app authors if they do almost exactly that...except replace that very last step of "and publish them as separate packages" with "and add them to the platform." :smile:
as an aside, a tricker web-specific problem I don't have a good solution for is the fact that (despite my attending a bunch of TC39 meetings on the Temporal proposal for the sole purpose of repeatedly explaining Elm's use case and why they should offer a way to access time zone data as plain data and/or pure functions, as opposed to a mutation-y JS API, which is what they ended up doing anyway), some Web APIs like Temporal and Intl have a bunch of data (time zones, internationalization info) that's already in the browser and which you don't want to re-bundle in your application binary...but it's not easily accessible in the format you'd want (constants and/or pure functions), which means accessing them in Roc unavoidably requires effectful functions
that might be only a mild annoyance in practice, given that you'd probably be wanting to do them in the same place as where you'd be doing other effects like I/O, but it bugs me that the browser ships with a large quantity of immutable data and pure functions but doesn't offer APIs to allow access to them as plain old readonly data and/or pure functions :disappointed:
a related annoyance is that browsers also have all the Unicode info you could ever want, but all their APIs on them only work on UTF-16 encoded strings :upside_down:
My concern with Roc is that the current platform design decisions push it towards being a deeply
parochial language. That is, every shop that uses the language uses its own version of the language
and while you techncially can share code in practice the friction is high enough that people tend
not to. The main example of this is C++ compared to Rust. Other languages with deep ecosystem splts,
and I'm mainly thining OCaml here, have enough confusion around what to pick that it's a barrier to
entry. I think in part it's the domain since I don't think Lua has a particularly large ecosystem
even though it's fairly popular. I've written some OpenResty code and know LuaRocks is around
but my general impression is that Clojure (to pick a similarly popular dynamic language) has a
significantly larger ecosystem on top of riding on its host platform for long tail coverage. Roc
platforms, as they exist today, are a push towards this future.
The core problem is that platforms are a total world and you have to take it or leave it.
There's no way to add to a platform: if I'm writing an app server and want sqlite, it either has to
be built into the platform or I have to fork the platform. I can't piece together functionality from
multiple platforms. There's no way to replace just a piece of a platform. If I decide I want Turso
instead of sqlite because I want to do repilication, I have to fork the platform. I'm not completely
sure about this, but I can't shim a platform effect in Roc code. The story for leaving things out
of the platform is dead code elimination. This is likely a psychological issue more than a practical
one but years of Java logging system exploits have left me wary of surprise networking in libraries
so it really bugs me that fetch is in my UI platform and available to a calculator app even though
I'm pretty sure there's no lurking exploit in the calculator. All in all, either the platform author
fully anticipates all your needs or you fork the platform.
There's a lot of platform forking motivations in the previous paragraph but I strongly suspect most
platforms won't have a carefully thought out domain. In my case I've done a half dozen and my
general decision for placing it in Rust or in Roc is which is easier or which will let me avoid
a copy. I expect this sort of motivation to be the norm which makes forking subject to the usual
subtle incompatibilities of an implementation-defined contract. As a specific example, my seahaven
platform mostly mirrors basic-cli because I wrote the app against basic-cli and wanted to add
sandboxing. Since I want seahaven to be cross-platform I switched paths from OsStr to Str which
trades the ability to fully represent every possible path on a platform for a stable cross-platform
representation. I don't think it'll make much of a difference in practice and it does show up in the
type signature but it's representative. It becomes difficult to come up with a better version of
basic-cli or basic-webserver because the person potentially swapping platforms doesn't really
have support for what could be off. It amounts to "good luck, hope your test suite is good enough."
The good thing about all this is that the WASM people are in more or less the same situation and
they've been working getting their component model standardized for years. I'm not arguing that Roc
needs to match their decisions but I do want to try to TL;DR the general ideas and I'll use their
terminology since there's a decent amount of thought behind them.
The WASM component model starts with WIT
which is their interface definition language and, in brief, it's simplified Rust types and a richer
cross-language ABI than C. Individual definitions get bundled into groups called interfaces
and the interfaces get bundled into worlds.
A wasm component model world more or less corresponds to a Roc platform. The main difference is that
worlds can both import interfaces and export interfaces. This gives clear boundaries for what a
program actually requires. I know there's been effort on getting HTTP types working in Roc so I'll
point to the wasi:http world
which has clean support for just making requests wasi:http/client, serving requests wasi:http/handler,
and a full service with clocks/random/stdout/stderr in wasi:http/service. There are similar specs
for other relevant domains.
This is significantly more complicated than just importing a package but I think a Roc version of
the same concept wouldn't have to present itself as more complex to new users. They'd just grab
http/service and write their server. I see this mostly as a way to have a coherent library system
across platforms and to allow me to produce a UI platform without a constant struggle around capabilities.
I have 10 apps in my project repo exercising the APIs with three platform forks servicing them.
I think the platform design might actually help with fragmentation a bit. Two of the most fragmented language ecosystems I have personal experience with, JS and Haskell, both have big package ecosystems. This gives everyone a large menu to put together their own stack: choose a database, a REST library, a logging library, a templating language, etc. In the case of Haskell your choice of language extensions to enable also plays into this.
Ruby, which is more framework oriented, arguably sees less fragmentation. You have one dominant framework (Rails) and maybe a couple smaller ones. Roc might end up similar, with one or two dominant platforms in each domain. It'd be great for app authors too, if the effort to add support for some useful functionality (say: replication) ends up being contributed to the platform most people are using, instead of being in a separate package that app authors need to bolt on themselves.
yeah, I thought about this back when the language was in the stage of "design but no code exists" and my conclusion was that "this is just how ecosystems work anyway" - e.g.
those aren't programming languages, they're engines/frameworks/platforms/etc. built on top of languages, and they emerge organically from languages that don't have a formal concept of these things
the idea is that Roc is taking something that has emerged in an ad-hoc way, and unlocking benefits (primarily performance and security) from making it first-class
The difference is that in all of those examples is that they're not a closed system. I don't have to fork Rails if I want my app to do image processing.
:thinking: why would you need to do that in Roc?
I have a native UI platform and my set of apps include audio playback, sqlite, pen EMR interaction, and web access. What set of effects do I ship in my platform?
Do I really need to re-implement libpng in Roc?
what's pen EMR?
It's the technology behind wacom tablets. The pen magnetically resonates with the surface allowing for hover/tilt/rotation in addition to pressure.
When working on a platform I have like the main idea of what I want to accomplish but there are lots of other things that are incidental but because platforms are total I have to care about. UI apps are going to need file access. I don't really have any value to add there. Do I copy basic-cli code?
Karl said:
Do I really need to re-implement libpng in Roc?
I don't think platform authors should, but I actually did some proof-of-concept implementations of image parsing in pure Roc awhile back, and I've already ported libdeflate to pure Roc (I'm currently working on closing the perf gap; decompression is between 6% and 12% slower on libdeflate's own test suite than libdeflate itself, whereas compression is still much further off - we're at like half the throughput).
basically anything that only requires pure functions on bytes can be implemented in pure Roc, and there are security and ergonomics benefits to doing it that way, so I think that's long-term what we can optimize for. (in the short term if a platform author wants to bundle libpng, or if an app author wants to fork the platform to add that, obviously that's fine but also less ergonomic than someone making a pure-Roc implementation).
that might sound like an absurd proposition until you realize that "don't just wrap libwhatever, actually reimplement" has been successfully accomplished at scale in the JS ecosystem because browsers (like Roc) don't let you do arbitrary C FFI for obvious security reasons. As an example, you mentioned libpng and that one has been done in png-js in pure JS. A pure Roc one would run much faster. :smile:
Luke Boswell said:
Like maybe the drawing in terrocotta would return a list of draw commands the app should then be responsible for translating into the platform's API -- as opposed to taking the effects and using those.
that does not remove the View(Msg, texture, font, shader, frame) generic bloat because the layout still have to carry the "unknown types"
Karl said:
UI apps are going to need file access. I don't really have any value to add there. Do I copy basic-cli code?
I think that's the right approach today. I'm open to the possibility that someday there is a better way, but I think it's important that at this stage of the language we empirically test out the approach of "platforms do not depend on other platforms" to see how it works in practice.
Romain Lepert said:
Luke Boswell said:
Like maybe the drawing in terrocotta would return a list of draw commands the app should then be responsible for translating into the platform's API -- as opposed to taking the effects and using those.
that does not remove the
View(Msg, texture, font, shader, frame)generic bloat because the layout still have to carry the "unknown types"
oh I'm proposing that when you look at the docs for roc-ray, you basically see the union of all the modules that are currently in roc-ray and currently in terrocotta - like, they would just be fused together, so the generic thing would go away
so that way if someone is forking the platform, they get it because it's a part of the fork itself
oops, I just realized you were responding to Luke's specific comment from earlier, sorry!
Luke Boswell said:
Ok, here's another angle ... what if I wanted to make a Calendar widget in a package for roc-ray?
This is not possible in roc.
Here is the dependency hierarchy (arrows cannot go up).
![]()
Let's suppose terrocotta is in the roc-ray platform as Richard says. Aka terrocotta is part of pf-package.
Then package A (e.g. widgets package) cannot import the most basic building blocks of terrocotta (e.g. import rr.Element exposing [box, image]).
That means a widget package is impossible.
Now :
component or a hook... StatelessWidget/StatefulWidget...createSignal(), createMemo(), createEffect(), etc...it simply does not exist.
all that is possible is copying example code from others like the C community.
@Romain Lepert as discussed earlier, you can do it with type variables, where clauses, and structural types...but what would be your preferred way of doing it? allow packages to depend on platforms directly?
(implying that the calendar widget would work with exactly roc-ray but not on any forks or API-compatible alternatives to roc-ray)
yes, allow packages to import pf-package. That unlocks the rest
oh interesting
so not declare a dependency on a specific platform, but rather let them import "whatever the app's platform is"
and then as long as everything type-checks, you're good? :thinking:
Or maybe something like a "plugin" bundle-type, separate from app/platform/package, that is a "library" written for one specific platform with access to that platform's API? That way you could support widgets, while maintaining the concept of packages for cross-platform use-cases.
I was thinking the specific platform's pf-package but what you are saying is more flexible, albeit a bit unclear.
with this hypothetical
import platform.Element exposing [box, text, View]
Msg : [Increment]
button : Str, Msg -> View(Msg)
button = |label, msg| {
box({ events: [OnClick(msg)] }, [
text(label),
])
}
what does it mean to that box, text and View type check ?
yeah there would be a few problems with that design, I guess - e.g. you could only type-check it if you had an actual app present
actually, I wonder if this overlaps with a separate design I've had in mind for a long time
Jasper Woudenberg said:
Or maybe something like a "plugin" bundle-type, separate from app/platform/package, that is a "library" written for one specific platform with access to that platform's API? That way you could support widgets, while maintaining the concept of packages for cross-platform use-cases.
that is basically the pf-package in my diagram, isn't it ?
which was letting app authors override dependency URLs, for purposes of doing things like patching an indirect dependency to see if a bugfix works
like if I depend on 2 packages which both depend on "https://foo.com/whatever/..." and I want to override that foo.com/whatever dependency with a local version, and have my 2 packages that depend on it get my overridden version, it would be good to have a way to do that
and of course as soon as you do that, as the app author, you're taking responsibility for the override working properly with those 2 packages you didn't write
and I think the "make it work with a fork of the platform" could be the same situation :thinking:
e.g. the calendar widget formally depends on a specific version of a specific platform, but the app author can override that and say "actually anyone who depends on that platform, use my platform instead"
so then the app author is taking responsibility for any compatibility issues etc.
so, tradeoffs compared to a platform-agnostic calendar package:
a thing worth noting here is that a part of how we ended up with the current "make all packages platform-agnostic" approach was starting from a position of asking questions about how to make packages that could work across multiple platforms even if they had different I/O primtiives, the canonical example being "browsers do network requests totally differently from how operating systems do, so how can I make an error-reporting package that works with Roc apps running in a browser and also Roc apps running on native desktop apps and also mobile apps etc.?"
and the conclusion was that the best option is to make them parameterizable
but it's notable that for GUIs, that sort of sharing isn't really a thing
e.g. nobody expects to write a React calendar picker that also works on iOS
so that coupling to the platform feels like it might be more innate when it comes to GUI widgets
which would be better justification for a language feature than the other things we've talked about imo
I'm curious what anyone thinks of this idea!
Richard Feldman said:
Romain Lepert as discussed earlier, you can do it with type variables,
whereclauses, and structural types...but what would be your preferred way of doing it? allow packages to depend on platforms directly?
In the example, terrocotta is part of the platform, in which case no package can import box which means they can't make a widget. where clauses and structural types can't help here.
A widget package is probably NOT the best motivating example. I would concede that most non-toy apps should own their widgets (shadcn showed the validity and desire for it). The only tricky ones that people don't want to own are the complicated ones like text inputs and calendar. However the main internals of these complicated widgets could be a package that every app reuses (e.g. unicode aware cursor navigation, d/m/year navigation)
But the point stands that you can't have a ecosystem around a platform if you can't import its basic building blocks.
Not clear to me what the idea is. Widgets have a couple axes of compatibility. The current trend for visual coherence is design systems consisting of design tokens. For actual painting it's either raw draw commands or some sort of property system; I use the tailwind system because frontend devs are aware of it. For behavior the ARIA grouping is the best platform agnostic system I know about. The final piece is how state is handled, both the app domain and the internal widget state.
the idea is to let packages depend on a platform
e.g. today if you make a package and try to use the platform keyword in your dependencies, you get an error saying that's not supported
the idea would be to say that it is now supported, and the enforcement would move to being that your app can only depend on packages which use that keyword if they are using the same platform as what you (the app) are using
so that's the idea of the language change, and then that would mean you can now publish platform-specific packages, whereas today it's only supported to publish platform-agnostic ones (which you could of course still do just like today)
the separate (but related) idea is to make it possible for app authors to say "if any of my dependencies are using this URL, instead use this other drop-in replacement dependency, and I will take responsibility for any incompatibilities" - which would mean that if someone publishes, say, a calendar widget for the roc-ray platform, someone else who is using a fork of roc-ray (or drop-in replacement), they can override that calendar widget's roc-ray dependency to use their API-compatible platform instead, and it can Just Work as long as there is no actual API incompatibility in practice
I have a Regex implementation cooking and one of the problems there is SIMD scanning for letters. I had planned on just not doing it and waiting until the discussion of the primitives came up on zulip but being able to depend on a scanner from the platform would let me do it now.
Perhaps related but over the weekend I was doing perf tuning for my web platform. Doing everything with encoders/decoders lets me get down to only needing copies into the SQLite results buffer and into the transmission buffer but the SQLite buffer is (obviously) host side and getting it into Roc requires a copy. I settled on making a non-effectful host function which works but gives a warnint.
we have SIMD builtins right now - are they not sufficient?
e.g. https://www.roc-lang.org/docs/main/Num/#U8x16
so let me try to see if i understood
1) calendar package depends on rocray: platform "https://roc-ray.com/v1.0"
calendar can import rocray.Element exposing [box]
2) Bob forks rocray into bobray
4) Jack wants calendar but with bobray platform
bobray: platform https://bob-ray.com/v1.0"
calendar: https://calendar/v1.0"
Jack patches indirect dependencies "https://roc-ray.com/v1.0" with "https://bob-ray.com/v1.0"
Is that about right ?
exactly, assuming terrocotta is a separate package from roc-ray (which for the record I still don't think is the best experience for app authors, but it's up to you :smile:)
Good morning everyone... looks like there's a bit to catch up on here :grinning_face_with_smiling_eyes:
Richard Feldman said:
the idea would be to say that it is now supported, and the enforcement would move to being that your app can only depend on packages which use that keyword if they are using the same platform as what you (the app) are using
Well I guess I know what Im building today now... I can spike this out and test it out on terrocotta.
I feel this is a natural fit with the package ecosystem. I was thinking about the discussion overnight and there seemed a hole here that this will fill nicely.
A message was moved from this topic to #performance > SQLite owned buffer by Richard Feldman.
Richard Feldman said:
exactly, assuming
terrocottais a separate package from roc-ray (which for the record I still don't think is the best experience for app authors, but it's up to you :smile:)
right, i modified the example with calendar so we focus on the substance :sweat_smile:
Theres an old google doc floating around with this design I think. (the patching deps thing)
Does it make sense to make this strictly a platform dependency concern? I was thinking about the problem with the upwards polluting generics (View(msg, texture,...)), and if the generics escalate to the top level, essentially your whole package depends on some set of types. Why not instead say "this whole library is parameterised by these modules (that must provide these functions)". Then, when a consumer requires the generic library in their package header, they specify the module to be mapped to. I feel like this suits Roc well because
E.g.
app [main!] {
pf: platform "...roc-ray...",
calendar: "http....",
with [calendar(pf.Element, MsgWrapperWithLogging, ...)]
}
Just for what it's worth :sweat_smile: I had been mulling over it for a bit. Don't know how it would work with non-opaque nominal types though or any use case that requires the flexibility of a concrete type. Essentially it is syntax sugar that hides wide spreading generics.
We previously had a design "module params" and then removed that in favour of record bundle of effects
@Romain Lepert -- I've started working on a branch to test out the "platform packages" concept. I'll patch terrocotta to use that instead of roc-ray/types and we can use that to help explore the design space
Here's the PR porting terracotta across to use the platform instead of the package... :sweat_smile:
https://github.com/obust/terrocotta/pull/60
![]()
I don't quite know what I expected to be honest... just more I guess :smile:
Here's the spike for Roc https://github.com/roc-lang/roc/pull/11079
I'm not really sure where to go from here ... I guess I could look at pulling the types package back into the platform and make another RC?
@Romain Lepert I'll wait for you to have a look at all this.
I think the reason Elm doesn't have full API coverage over the Web isn't that it's fundamentally unachievable, or a bad idea, but rather prioritization of other projects ahead of that one.
@Richard Feldman this is my point actually. Nothing is literally impossible. Things are just too complicated, costly, time consuming to accept the weight of doing and maintaining them (or the opportunity cost here more specifically). And from the point of view of a contributor, if they want to add a new capability to a platform, the difference between "I can just do my thing and we good to go" and "how do I patch the whole platform to integrate my thing?" is the difference between an existing contributor, and someone who decided it’s not worth their time and move on to something else.
That being said, I’m not opposed to still try it and see how it goes. It’s just that from my point of view, I feel there are intrinsic reasons why easy extensibility is needed.
@Karl thanks for mentioning wasm WIT! I think it’s a very good comparison. Pure wasm, with side effects provided by the "platform" in an extensible way.
@Karl In the chat after the last meetup we discussed your thoughts on wasm WIT. Would you want to write out a brief/suggestion around that in a separate thread? Based on our discussion it did sound valuable to bring up with everyone.
WIT is certainly a good comparison point. Putting aside the
A platform quite litterally builds an interface. Aka just types and function definitions, no implementation.
e.g. roc-ray Host interface which is then implemented through FFI binding.
Host := [].{
Handle :: Box(U64).{
stub = Handle.(Box.box(U64.highest))
} # only platform can manufacture Handles
Texture := { handle: Handle, width: F32 height: F32 }.{
stub : Texture
stub = { handle: Handle.stub, width: 0, height: 0 } # for tests
}
draw_texture! : { texture : Texture, position: { x: F32, y: F32 } } => {}
...
}
So a package could define the interface it requires and as long a the app picks a platform that satisfied this interface, then the package is compatible and it can use the interface.
That would be essentially what @Richard Feldman mentioned here:
so not declare a dependency on a specific platform, but rather let them import "whatever the app's
platformis, and then as long as everything type-checks, you're good?"
However there is 2 issues with that:
1) Current Roc cannot match package declared Texture.Handle with platform declared Texture.Handle because of the nominal in nominal. I don't know if that restriction can be alleviated or not.
2) It does not play well with roc-ray intended effect usage. roc-ray apps have a lifecycle init!, update!, render! and draw_texture! calls are valid only in the render! phase, otherwise it will crash the app. roc-ray can design its API so that it is hard/impossible to call draw_texture! outside of render! but if it lets a third party package call draw_texture! whenever it wants, then it can crash the app. It would be either a bad package design from the package author or a bad package usage from the app author, but still it is harder for the platform to own the UX.
Given point 2) I would not go that route just yet. Let's see what platforms will come up with first. But curious to know if people think there is a way without compromise.
Luke Boswell said:
I don't quite know what I expected to be honest... just more I guess :smile:
ahah well that is because roc-ray/types was filling this gap.
what roc-ray/types could not expose was Draw.Frame with the drawing effects. That means that now a package can import Draw.Frame and write a render function using the frame effects. No where clause required. This make is straightforward to build a Chart package for roc-ray for example.
Luke Boswell said:
We previously had a design "module params" and then removed that in favour of record bundle of effects
I think what I’m suggesting is a generalisation of the idea in this thread: depending on a platform is effectively parameterising your library by the modules of a platform - why not make that dependency explicit, precise, and remove the limitation to platform modules? Doing so buys you the ability to easily test these coupled libraries, as well as provide adapters for when they don't exactly line up. This is similar to Haskell’s Backpack, and is distinct from (and does not tackle the problems solved by) module params/bundle of effects.
I did a bit of Zulip archaeology for the context of the module params proposal and subsequent abandonment. As far as I understand it: module params were a way of feeding in values to modules, e.g., keys, or effectful functions. At the time (pre static dispatch), they were especially useful because there were no custom types or ergonomic ways to destructure and call a function contained in an open record. Then, with Richard's realword exploration, the bundle of effects pattern was found to solve these same problems, and even provide more flexibility, e.g., log configuration by passing in a closure, or request safety by passing in a pre specified domain. Module params were removed thereafter as part of the rewrite, because it seemed like static dispatch could replace them, as well as abilities and default record fields.
But what is now being explored, is that for a sufficiently platform-specific library, neither the value-level (ergonomic bundle of effects) nor the type-level (static dispatch and generics) options are satisfactory. I.e. good tools to have but not sufficient, because it requires either passing these values down the stack, or where-constrained generics all the way up the stack.
For this kind of library that is tightly coupled to a platform, where it is burdensome to pass around a complex set of effects, the main idea in this thread is to be able to import from the platform. Essentially, parameterise your library by the set of modules that a platform exposes.
I’m arguing that this idea can be generalised: libraries could be parameterised by modules, rather than just platforms. So, no way to do precise (dynamic) configuration, but answers the problem of "what if my library assumes this other set of modules that I want to import”, without limiting it to the subset of "what if my library assumes this platform”, or having to pass around the types/values.
The use cases for this generalisation are
X, it should use my module Y, which adapts it to pf.Z"calendar library is not just accessing Texture but HTTP. This would not be possible if libraries were parameterised explicitly by the modules they depend on.As for complexity budget, it is simpler than module params or functors, because you parameterise once at the library import level. Compared to importing platform types, it makes the dependency on the module more explicit.
Hypothetically it could work if the limitation mentioned here was somehow lifted
1) Current Roc cannot match package declared
Texture.Handlewith platform declaredTexture.Handlebecause of the nominal in nominal. I don't know if that restriction can be alleviated or not.
I think those are the same thing - the modules you're talking about are basically types :smile:
in other words, saying "I depend on a module named Draw which exposes these associated types and functions" is the same thing in Roc as saying "I depend on a type named Draw which has these associated types and functions"
I think they're close; I suppose you could pass in a handle to the module that provides access to the module via methods on the handle. I do mean this at the library level though, rather than parameterising the modules individually, so that the library authors can import and use as normal.
so something like "to depend on this package, you need to specify an existing Draw, which has these methods, and Texture, which has these other methods, and..."
and then those can be nominal types
It also reduces the onus on the app developer to wire together the precise set of effects, instead just specifying the module. This configuration for tightly coupled libraries was one of the complaints of module params I saw.
Richard Feldman said:
and then those can be nominal types
And importantly you can reference them normally in the signatures of your library functions
Jonathan said:
It also reduces the onus on the app developer to wire together the precise set of effects [...]
but you can still do so with bundle-of-effects, which should still be the goal for most packages that are agnostic.
Niclas Ahden said:
Karl In the chat after the last meetup we discussed your thoughts on wasm WIT. Would you want to write out a brief/suggestion around that in a separate thread?
I've mentioned the wasm component model a number of times; I got into Roc because I was specifically looking for a wasm language and I thought Roc's design could be a particularly good fit. In general I think Roc could be a particularly excellent platform scripting language if it could read the types off existing modules and have that be treated similarly to host effects. I think the nice has a good potential to be valuable and that there aren't really good competitors for it so that's part of my motivation for pushing the module system in that direction.
More relevant to this discussion, I really would prefer that a platform be a composition of fairly tightly scoped interfaces. I could then write crates to line up with the interfaces and if there's some way to declare the mapping I think that would be fine. I want to be able to ship a UI platform that doesn't have to care about filesystem and network access, drivers, and dealing with large chunks of native code. I think telling someone who wants a database with their GUI to install Rust and cargo add roc-sqlite is reasonable (yes this is quite fuzzy, I haven't thought through it in depth). I think telling them to learn Rust and fork the gui platform is not reasonable.
When I talk about this I tend to veer into hypotheticals for wasm-scripting use cases. As an example, let's say I write some Roc to do analysis on NYC taxi rides. During local development I can read/write files using the normal posix interface with a CSV decoder. Once it's working, recompile it to wasm replacing the file read and CSV decoder with a fetch from the online dataset and an Arrow decoder, swap the write for a put to S3, delploy the wasm to some function as a service host and it should work. There's some handwaving about decoders but if the interfaces are fairly small I think swapping out the file read and file write effect is something people would actually do.
This is completely unnecessary for my current problem, which is how should a GUI platform handle non-GUI effects. There are common ones that could put in the platform (would prefer not to, IMO it's out of scope) but the long tail here is basically unlimited.
As I understand it, the argument is that the platform can expose sockets and filesystem. Then everything on top is about reading bytes which can be pure roc. So the long tail is to rewrite everything in roc: database clients, http, SQLite?, etc… then the platform composability becomes less and less relevant. Is this feasible ? I don’t know :man_shrugging:
SQLite is an extreme outlier, so let's set that one aside for a sec
(happy to discuss why it's an extreme outlier, just really doesn't apply to the rest of the topic)
for database clients, every language needs to have a concept of a db client - e.g. "here is how to talk to Postgres in Ruby"
similarly, an http client - e.g. "here is how to talk to servers"
in all of these cases, almost all the work that needs to be done is encoding bytes, sending them over a socket, and decoding the bytes that come back - e.g. "how does postgres represent an integer on the wire? how about a row? how do I convert those bytes to/from useful Roc types?" is a lot of work, but doing that work is the only way to get the best performance and ergonomics
if you use libwhatever as an intermedary, you're going to get worse performance because you're double-encoding and double-decoding (with the C library's representation as the intermediary)
so if you want, you can have a platform that bundles postgres support, and mysql support, and redis support, and does all of these by using C libraries as intermediaries, but I would consider that to be a short-term stopgap - like "this unblocks people being able to use Postgres on my platform right away, but obviously the better long-term thing is to have a pure-Roc postgres client and then I can delete my baked-in stopgap and tell people to use that"
so separate from the byte formats, there's also the question of I/O
when doing a native application, you just use a socket to talk to databases
but inside a browser, you have different network APIs (maybe less relevant in practice for databases specifically, but this applies to anything that fits the pattern of "encoding/decoding bytes over the network is most of the work")
so being able to make a platform-agnostic db client that's like "tell me how to send and receive the bytes, and I'll do all the hard work for you" should in theory be really amenable to the "bundle of effectful functions" strategy that Bugsnag and similar can use, because you only need ~2 effectful functions
and you also need a persistent db connection value to pass around anyway (under the hood, likely a 32-bit socket handle), so the only ergonomics cost is just having to pass another argument to specify the I/O operations in question when initializing your db connection (as opposed to the postgres client just ambiently "knowing" how to do socket I/O and similar on other platforms)
regarding the question of "I want to make a GUI platform, and there's nothing interesting about my reimplementing all the I/O primitives" - I completely understand where this is coming from, but consider the two alternative worlds here:
This is the world we live in today: there is no language-level special mechanism for combining platform primitives. What are your options?
If there's an existing platform you like in terms of I/O (e.g. basic-cli), you can copy/paste it and then just start changing the parts you like. This obviously works, and is fine for application authors, but maybe is not the platform-authoring experience you want.
Another option: ask the authors of basic-cli to publish Rust crates for its Roc I/O primitives on the host side, so now your host can just import that crate. This is more convenient for you than copy-paste, although it's more work for the authors of basic-cli. It also doesn't keep the Roc APIs corresponding to those I/O primitives in sync with the host implementations.
Another option: ask the authors of basic-cli to publish not only their Rust crates, but also, alongside it, the corresponding Roc code. This doesn't have to necessarily be a Roc package - could just be the relevant source files so you have them all in one place.
Roc gains some sort of "platform plugin" concept. It becomes possible to publish a package of "host implementations plus the Roc APIs that go along with them," so that platform authors can pick from a menu of these and combine them together to assemble a platform.
So what has been gained? Today, platform authors are not in any way blocked, and there's full access to all the building blocks that exist. So we're talking about an inconvenience at worst.
What has been lost? We've added a gigantic amount of complexity to the way platforms are built, and to the language (Roc now needs to know how to build whatever host language people are working with, and/or introduce the concept of arbitrary per-package build scripts, exposing platform developers to a wealth of corresponding security vulnerabilities that we do not expose anyone to today).
We have also introduced predictable follow-up problems ("this plugin uses Zig for the host but I use Rust, so we need a way to mix and match" is extremely predictable, but modern systems languages often do incompatible static compilation things where one of them needs to be "in charge" so that you don't have them stomping on each others' global initialization - e.g. setting up threadlocal storage - and mixing and matching them is very prone to problems).
We are also completely changing our dependency culture around how platforms are built to be more "as the platform author, you are taking responsibility only for the part of the platform you're interested in, and everything else is delegated to others; if an app author has a problem with the platform, they can often expect to see their bug report redirected to some platform plugin repo they've never heard of" instead of encouraging "as the platform author, you are in full control of the entire foundation apps are built on; with great power comes great responsibility" which has always been the primary goal of the platform/application split.
maybe someone can propose a specific design that doesn't have these downsides, but I think this is on the extreme end of "cure is worse than the disease" and I would be very surprised if there turned out to be a "platforms and their hosts can be assembled from first-class building blocks in the language" design that seemed better than the status quo we have today.
I'm not saying we couldn't find ways to improve the status quo, just that "composable building blocks" feels like a complete dead-end to me, even though it might sound appealing at a surface level before you actually get into the specific implications of what it would mean if we had it.
especially when you remember that the status quo is "everything works, platform authors are unblocked, app authors get the best experience" and the only problem being solved is "copy/pasting parts of the platform I'm not interested in is annoying"
I just think that annoyance really pales in comparison with the problems we could have if we tried to alleviate the annoyance in this way :sweat_smile:
This makes sense to me, but there is a clear tension between "as the platform author, you are in full control of the entire foundation apps are built on; with great power comes great responsibility" and the options in World 1. If the support for making this smoother doesn't come from Roc directly, I suppose it would have to be in external build systems and tools like Glue?
As in, design patterns for being able to extend platforms, rather than making it a plugin-based first-class system.
honestly, part of the reason I'm not that worried about the long-term ergonomics there is just that I don't expect the I/O primitives to change that often
My takeaway is that I'll need to solve the problem in Rust. I can do that.
like if you start off by literally copy/pasting your I/O primitives and corresponding Roc APIs from an existing platform, how often are you gonna end up updating those?
I wouldn't be surprised if you just leave them alone for a long time and just focus on updating the parts of your platform that you find more interesting anyway
I'm less concerned about this for my own sake and more concerned about people using the platform. There is honestly no limit to the amount of things you'd want to do with a GUI platform. The point of the GUI is to provide an interface for whatever domain you're working on. The LLM s can do most of this but I really want a smoother difficulty curve than "you're outside the bounds of the platform, figure out everything yourself".
Richard Feldman said:
honestly, part of the reason I'm not that worried about the long-term ergonomics there is just that I don't expect the I/O primitives to change that often
Yeh, I don't think it would be likely to come up, but in the back of my head there's this risk that I'll reach a part of a project and find that the platform doesn't meet my needs. I would rather not work on the platform at all sometimes -- I have enough problems with bikeshedding already, and am not adept at lower-level code.
I have distinct memories of being a university student and running into stuff like this. It's not that I couldn't do it, it's that I needed some path to follow.
yeah there's definitely a catch-22 aspect to this: once you have an ecosystem of fleshed-out platforms people can look at, it becomes easier to add the next one because you have lots of examples to draw on...but of course, when building those out in the first place, you necessarily don't have them yet
but the flip side of that is that if you're hitting a specific issue with your platform, there's plenty of bandwidth and interest from people in this Zulip to help you out! :smiley:
It would be nice to list out all the things that we expect platform authors to be responsible for, because this list is vague to me at least. The platform controls:
I believe. Anything else?
that sounds right to me!
Luke Boswell said:
Here's the spike for Roc https://github.com/roc-lang/roc/pull/11079
FYI all I just merged this... I will update roc-ray to remove the "types" package
I don't want to be a stick in the mud, but I didn't realise this was a settled matter :sweat_smile: I think it will be interesting to explore for sure, but I still feel like it would be better to try and find a mechanism that makes sure libraries are never directly connected to platform effects, for the aforementioned safety/extensibility/visibility/testing reasons.
WASM WIT, seems like a good candidate for this.
https://component-model.bytecodealliance.org/design/wit.html
A collection of interfaces, bundled together as "worlds".
"Stdin/out world", "Filesystem world", "Network world", "Graphics world"... etc.
"Compose-able roc platform"
Could generate a thin roc interface platform, and zig function stubs... or C .h header files. And then build up a collection of libhost.a "world" files... and then glue together on demand... + malloc and other minimum runtime requirements libhost.a as the "minimum runtime world" host.
Sometimes you have "cross-world" concerns.
"I want to constrain, and cache the disk & network, in this specific combined way for performance or security or whatever".
And other times you, just want a quick scripting environment, and would be happy with a roc WASI (wasmtime, wasmer) like generic host platform.
I think the current design and situation is fine... once more platforms arrive on the scene... can easily just fork... and just add that 1 extra effect that you need etc.
Maybe the thing, is just more nicer tooling and examples to glue two big platforms together... to create one big monolithic platform.
"I want the godot game engine AND a web server AND to write to this one specific filepath."
I think it's just a tooling and ecosystem problem that will be solved evenutally, not a design problem.
The interface between host/platform & roc app... is customizable, it's literally just the C-FFI. Or rather, the problem has already been solved.
Maybe just a collection of C header files, just maybe ones that are compatible with roc ABI, so we don't have do any mapping, translation or conversion. It would be nice but wishful thinking.
I guess that's the real unavoidable factor... "I want to combine godot game engine, with roc. Godot doesn't know about roc ABI... now i need to convert between godot and roc..."
it hasn't been too painful... with the zig helper functions existing... It's not that bad... especially with LLMs these days, can do alot of the heavy lifting and knowledge work.
More glue, more generators, more tools, more composable hosts, more hosts with pluggable systems...
I think that's the main thing... an example of how to glue two small hosts together, just stdout and another for just filesystem. And having a tool that generates a platform or combine two platforms. Once a proof of concept has been made and published... others can follow the recipe.
TLDR:
an example of how to glue two small hosts together, just stdout and another for just filesystem. And having a tool that generates a platform or combine two platforms. Once a proof of concept has been made and published... others can follow the recipe.
I'll try this... just clone the zig platform template twice... and try combine two libhost.a together... maybe the linker can just do it... or just need to specify "merge conflict resolutions", and once that's done... the recipe can be re-applied when you pull upstream changes. "Always use malloc from platform B", or from "my minimal host-shim".
And it's a .dll or .exe project, with roc_main as entry point... just a few configuration options... that only need to be specified once... in a build_config.roc ofc.
HostInfo: {}
PlatformInfo : {}
MergeConfig:{}
BuildConfig : {}
host_a = { a:"libhost_1.a" } # webserver
host_b = { a:"libhost_2.a" } # godot game engine
merge_config = #...
platform_a_plus_b = build(build_config, merge(merge_config, host_a, host_b))
Last updated: Sep 24 2026 at 15:59 UTC