We have simplified the syntax for app modules and interface modules, but we didn't get to the other module types yet. Currently the hosted module header is
hosted Host
exposes [...] imports [...]
In the style of interface modules, we should move to
hosted [...]
where the [...] is the "exposes" list, and we use import statements like every other module type
We will have updated every module header except for platform modules, and I'm not sure how to reduce those
platform "cli"
requires {} { main! : List Arg.Arg => Result {} [Exit I32 Str]_ }
exposes [...]
packages {}
imports []
provides [main_for_host!]
This is the current one for basic-cli
with the exposes section elided
We can remove the "cli" name, remove "imports" in favor of import statements, and combine the two "requires" sections, but it seems not reducible after that
platform
requires { main! : List Arg.Arg => Result {} [Exit I32 Str]_ }
exposes [...]
packages {}
provides [main_for_host!]
I think removing the words would just get confusing, but maybe there's something I'm missing
The one other option I can consider for that one is to put everything in a single record for visual consistency with all other module types only having one or two sets of fields:
platform {
requires: { main! : List Arg.Arg => Result {} [Exit I32 Str]_ },
exposes: [...],
packages: {},
provides: [main_for_host!]
}
Thoughts?
When you are changing this both headers, could you consider the problem, that it is not possible to import the requires
symbols in any other module of the platform. Especially not in the hosted module. One consequence is, that it is impossible to have effects that have a Model
as argument or return type.
For example, it is impossible for an platform to implement an STM as discussed in this thread. It would need effects like
get_model_for_reading! : {} => Model
or do_stuff_with_Model! : (Model => a) => a
. But currently, it is impossible for the hosted module to import the Model-symbol.
So my question is, could/should the hosted module header also have something like an requires
argument? Or should the requires
feature be redesigned in a way, that solves that problem? (and therefore would need a different header)
I can understand wanting there to be platform functions that can access the requires
types, but I don't think it's viable for the hosted module to send Model
to the host.
FFI needs to send/receive statically-sized types, and Model changes depending on the user's code outside of the platform
Currently, we compile the platform before even sending it to the user as a code archive via GH releases, so there's not a performant way to make a dynamically-sized value available to the host
That said, we could make it available but not allow sending it over the FFI boundary, instead requiring the platform to serialize/deserialize to and from List U8
But then we're promoting low-perf behavior, right?
Would love to hear a lower-level expertised opinion on this
To send a Model
to the host, you have to box it. Its the same thing you have to do, when you use the Model
in the exported functions. So an effect would actually look like get_model_with_lock : {} => Box Model
and afterwords there could be another effect like release_lock : [Commit (Box Model), Rollback] => {}
. In this example, the host would not need to do anything with the Model, just return the pointer. And the Roc part of the platform could provide a function like call_with_model : (Model -> a) => a
that internally calls this both functions. But this function would also need access the the requires
part from the header.
(I think I highjacked your topic. If you think, that my posts are off topic, you could move them.)
Nah, they're on topic enough. I don't expect much resistance to aligning the format of the hosted
module header to the standard module
header, and this discussion could help us align the hosted
and platform
headers
So anyway, the Box
approach makes sense, thanks for outlining that to me.
I believe there was recent discussion surrounding the desire for type variables in requires
types, but we can't do it if we want all types used in the program to be concrete for runtime
As long as requires
types like Model
need to be concrete, then what you're suggesting makes sense
There was a spitball idea maybe 6-8 months ago from Richard to use modules params in the platform
module to define the host functions, rendering the hosted
module type redundant
Since module params may be going the way of the dodo, I don't think it's wise for us to assume we'll have them long-term
But something is of intrigue there: for each platform, the following things need to be done only once:
requires
types and values/functions that are passed to the host via the provides
clauseprovides
functions using the requires
entitiesroc-json
, weaver
, etc.) for the platformIn theory, all of these could be done in one, big module, but that would probably lead to having a really big module for mature platforms
Though as you point out, the current hosted
module separation prevents platform authors from being able to access requires
entities
I think we should either:
platform
and hosted
into a single module type (leads to bigger modules, but simplifies Roc a bit)requires
clause to hosted
modules as well, and give errors when platform
and hosted
disagree on the contents of the requires
clausesThere is another problem, with a combined platform
and hosted
modules. It is currently impossible to import stuff from it. For an platform to export something to the app, it has to be inside another module, so it can be imported with import pf.Module
. If the exported functions would live inside the platform-module, they could not be used be an app or any other Module provided by the platform.
I'd vote for the first one because it seems simpler, and I'm not convinced it'll be that much of a problem for big platforms to have everything in a single file since you tend to only have a couple functions that glue requires
to provides
, and the FFI definitions are only type signatures, not function bodies
you can import Stdout
from the platform
module
Oh, I see
A solution we could push is to combine the two modules and require the platform
module to be named Platform.roc
or something like that. And then everyone can always just import Platform exposing [...]
Maybe the way, stuff gets passed around between the app and the platform has to be rethinked :grinning:
If you do, another (off topic) issue is, how a platform-Module can define, that something is exported to the app or exported only to other modules inside the platform. In my previous example, the platform only wants to export call_with_model
. But the hosted-module exports get_model_with_lock
and release_lock
. How can the platform make sure, that the app can not import this effects directly? I think, basic-cli and basic-webserver use Modules with an Internal
-Prefix. But If I understand it correct, then an app could import the stuff from the Internal
-Modules as well.
An example where this is a problem is the current version of the kingfisher platform. The hosted module exports save_event!
. But the app is only allowed to use it in a write request as defined here. If an app just imports pf.Host
and uses this functions, all guaranties of the platform are off.
Go solves the problem with an internal
directory. Maybe Roc could forbid apps to import stuff from the hosted
(or combined hosted
and platform
) module and also forbid apps import from Modules, when the Name starts with Internal
.
Sam Mohr said:
A solution we could push is to combine the two modules and require the
platform
module to be namedPlatform.roc
or something like that. And then everyone can always justimport Platform exposing [...]
If this Platform.roc
-Module could also export Model
then my initial problem would be solved. And if Roc forbids apps to import stuff from the Platform-module, then I would be fully happy :innocent:
Note, we should be able to pass a Box a
to and from the host just fine. The specific problem is that we need to link the a
from two different effects. This is currently done through requires, but it doesn't have to be.
For example, it would be easier to deal with basic webserver if the Model
as actually model
. Then you wouldn't have to worry nexted about type variables and some of the typing conplxity
To do this, we just need a way to specify that model
from init!
and respond!
are the same type. The same would apply to many effects.
I think this would be far nicer to support in general and would prefer it over requires syntax.
Caveat. This only removes the use of requires for types. It would still be needed to pass app functions to the platform unless the platform just imports the app and calls something.
Note, we can work around this for platform main functions, but not for effects. For basic webserver init!
and respond!
, we can instead use a record { init!, respond! }
and use the record to link the type. It would be nicer to also link effect types.
Wow. I was fixated on the dogma that Model
has to be pass to the platform. Your idea seems much simpler.
I don't understand how this could work on effects or other exported functions. How could the platform specify, that the argument from do_stuff_with_Model! : (model => a) => a
is the same as the return value from the provided function init! : {} => model
?
Yeah, I'm not sure how we would wire it up, but fundamentally it is just propagating a constraint. (Just have to make that clear to the user and platform author somehow)
Dumb idea, but it could be a simple as a !
suffix links type variables in the platform
So:
do_something_with_model! : (model! => a) => a
And
init! : {} => model!
yeah I like the way Elm does it and would like for us to do something that too - where it's just normal type variables and that's it
e.g.
init! : {} => (model, (model, Request => (model, Response)))
so in that type, init!
returns both the initial model as well as the function which takes the current model and a request and returns the new model and a response
although as discussed somewhere else, that would have race condition problems :sweat_smile:
if handlers can run concurrently
You could do something like this to allow async model updates:
init! : {} => {
model,
respond!: model, Request => (update, Response),
updateModel: model, update -> model,
}
Wow, I didn't even read that link
yeah in Elm terminology it would be:
init! : () => {
model,
respond!: model, Request => (msg, Response),
update!: model, msg => model,
}
Does update need to be effecful?
doesn't have to be, but you might want to do logging or something in there
Sure
it does have to be single-threaded
as in, at most one update
function is ever running at a time
just going through the queue of messages
The problem with this approach is you no longer guarantee seeing updates to model before the next request handling happens, but also that's just the case anyway with proper multithreading
right
I think this API is pretty good
I definitely think it's good to use normal type variables for this
So we still don't need to expose model to the host? Is there a use case that would make that desired?
I'd love to get rid of the Model
concept we have today
actually it was originally a workaround
because we didn't have sending closure captures to the host working, and that seemed like a big project
so Folkert suggested adding this as a workaround to unblock the use cases :big_smile:
That's one more section gone from the platform
but it was never intended to be the long-term design
the other thing is that I suspect this pattern of init!
returning the request handler function is going to end up being what every webserver wants to do
because it's such a convenient way to get environment variables etc. on startup
and then you just close over them and that's it
I'm actually not sure if today it might need to use Box
in order to work around current compilation issues, not sure
we could try it out on basic-webserver
, see if it works as-is
I cannot see, how this solution would support effects that use model. You would need to define all effects in this init structure.
:thinking: what effects use Model?
Something like save_to_db! : Model => {}
or load_from_db : {} => Model`
Stuff for Kingfisher
ah I see
so a tradeoff there is that if you have to represent that using a value that you pass around - e.g. something like:
save_to_db! : Db model, model => ()
load_from_db! : Db model => model
...then it's less convenient in that you have an extra value to pass around, but on the other hand, without that, you wouldn't have a way to simulate database state during tests. You'd have to always the real platform functions
as opposed to being able to construct a fake Db
and pass it around
Richard Feldman said:
yeah in Elm terminology it would be:
init! : () => { model, respond!: model, Request => (msg, Response), update!: model, msg => model, }
What is the benefit of that over just exposing a record of functions to the host?
you can read env vars (and similar) and use them in any of the returned functions
You can also do that by storing them in model
But I guess this would mean that you could avoid storing queries in the model. They are constant after creation. That would simplify all the error managing.
yeah exactly
I guess that may be a reasonable api once repeatedly using closures with the platform just works.
Richard Feldman said:
so a tradeoff there is that if you have to represent that using a value that you pass around - e.g. something like:
save_to_db! : Db model, model => () load_from_db! : Db model => model
I don't get it. What is Db model
? Is this something Init
has to return?
Last updated: Jul 05 2025 at 12:14 UTC