@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.
Last updated: Sep 03 2026 at 15:16 UTC