I have been playing with the platform stuff for a week or so now and a couple questions have been nagging at me.
In the Roc FAQ it says:
it's possible to write Roc code that can be used across multiple platforms. Applications can use platform-agnostic packages, as well as packages involving I/O, as long as their platform supports the I/O operations in question
This seems problematic for very complex packages that most other languages hand off to some shared library or the OS. Some that come to mind are:
While it seems like no problem to create a platform that supports any one of these the issue would seem to arise in composition. With the ecosystem today if I want a sqlite database for my application I need to use basic-webserver. But what if I just need it in a cli context? At that point am I forking basic-cli to add it? Is every reasonably robust "general purpose" platform expected to embed sqlite? I don't see a world where sqlite or QT get re-implemented in roc. Maybe this isn't as big a problem as i think since the Rust package ecosystem acts as a pseudo package manager for platforms? Could some of the module param work I have seen happening provide "feature flags" for platforms?
If you have read this far know that I am not trying to be antagonistic. I love the platform concept and understand that every design has its limitations. I am just trying to understand if I am missing something.
The above quote implies that so long as there is a mechanism for it in the platform packages can be effectful. Has there been ideas around standardized effects across platforms? POSIX, Python's WSGI, and Java's ODBC come to mind.
I think your concern will be shored up pretty soon, if I understand you correctly.
Given that IO and other effectful operations can only be done by specific platforms, the language as it currently is does not support packages that do IO. However, as is described in this proposal, by adding the Task
(e.g. IO monad) as a standard library feature built-in, we make it possible for packages to take a function that runs IO and return a Task
that composes an operation using said Task
. For example,
loadJsonDataFromApi : Str, (Str -> Task (List U8) err) -> Task Json err
loadJsonDataFromApi = \url, getData ->
data = getData! url
parseJson data
This would now be able to go in a platform-agnostic package, and all a caller would have to do is use some Http.get
from their platform and do
import cli.Http
import Package exposes [loadJsonDataFromApi]
myData = loadJsonDataFromApi! "http://api.com" Http.get
There's already a PR for this, we just need to finish testing and then you can do the above!
You will notice that this is a bit clunky, though. What if you don't want to pass Http.get
or Sql.query
to every invocation of an effectful function imported from a package? Well, we have another proposal for that for module params, which lets us pass variables/functions to modules when we import them. That means we can define our package module as
module [loadJsonDataFromApi] { getData : Str -> Task (List U8) err }
loadJsonDataFromApi : Str -> Task Json err
loadJsonDataFromApi = \url ->
data = getData! url
parseJson data
and then just use it from a platform as such
import cli.Http
import Package exposes [loadJsonDataFromApi] { getData: Http.get }
myData = loadJsonDataFromApi! "http://api.com"
There's also a PR that just got put out today by the industrious @Agus Zubiaga for that, so wait a couple weeks and you can do this yourself
Once these two features (Task as a built-in and Module Params) get merged in, the ecosystem will be fully modular! I think the only thing beyond that I'd personally be worried about is Roc's understanding of whether two packages with different versions are API-compatible. What if the platform uses roc-url
v0.2.0 and I use v0.2.1? Currently, I can't pass my own opaque Url
because it's a type error, but once Roc understands they're the same thing basically, then it will be less annoying to use common external packages between a platform and an application. Relevant proposal
Thank you for the detailed answer. I have seen a lot of these PRs flying and I have been struggling to develop the image for where that makes the language end up when they are all landed.
Also, I think forking platforms to add new effects and customise things is encouraged :smiley:
Ryan Barth said:
Thank you for the detailed answer. I have seen a lot of these PRs flying and I have been struggling to develop the image for where that makes the language end up when they are all landed.
I think the language will have all the tools it needs, but we'll need to wait for a pattern to emerge on passing Task
context for complex scenarios. What if I want an ORM-style package that supports 3 different SQL implementations? How do I organize taking DB connection config if I want to allow a Str or a record? I have some ideas on how I will do it in Weaver and roc-uuid, but I'm not sure what will be the standard in the ecosystem yet.
I don't think Task as builtin or module params fix this.
I think they help in a specific case, but they don't fix it.
If you need SQLite, that has to be in the platform. If you need GPU driver bindings, that has to be in the platform. If you need sockets, that has to be in the platform.
While module params may allow you to build functionality based on a flexible set of primitives, it does not fix the limitations of every primitive needing to be exposed by the platform.
There is no arbitrary c ffi and that definitely is a limitation.
YouTube personalities are going to be talking about how they "re-wrote it in roc" in the future :smiley:
Theoretically, someone could make a wrapper around libffi to alleviate this, but it likely will be a complex to get write and nice to use. We maybe even should look into supporting it at the language level (still forcing tasks). Theoretically with tasks as builtins, we could allow arbitrary cffi via shared libraries. basically allow a package to define it's own hosted like module, but for shared libraries. That is probably something we should seriously look into for more flexibility.
I see module params more as alleviating mapping problems. It allows for a generic print function that might print to the terminal or might send an http request to a logging server. It can abstract over graphics primitives such that many different gui libraries and platform graphics solution can communicate with each other.
But module params and builtin task to not allow for fixing the issue of need primitives that are not part of a platform. The lack of platform composability makes development harder.
Theoretically, ready made host language packages and feature flags could be added to something like basic cli to support more of theses features, but today, forking is the solution.
Luke Boswell said:
uTube personalities are going to be talking about how they "re-wrote it in roc" in the fut
What's my plan hahah
Last updated: Jul 06 2025 at 12:14 UTC