I wrote up an idea for how effects in packages could work - any thoughts on it are welcome! https://docs.google.com/document/d/1QAX7lvCFXhtb6h_gHQPBi9ZzqhIt_HEh4fULAwJXQDw/edit?usp=sharing
Regarding ecosystem design:
Having the published non-platform packages be mostly/all platform-agnostic would be phenomenal! Enforcing it to be "all" sounds like a good/necessary approach to achieve that. I'm trying to think of a way to eliminate your stated drawbacks (particularly the package author's burden of agnostic-izing the code), but perhaps they're simply part of a subjective tradeoff.
Richard Feldman said:
I wrote up an idea for how effects in packages could work - any thoughts on it are welcome! https://docs.google.com/document/d/1QAX7lvCFXhtb6h_gHQPBi9ZzqhIt_HEh4fULAwJXQDw/edit?usp=sharing
I like the idea with passing all the effectful functions in as parameters though until I play around with it I'm not sure if it will be too inconvenient or if that won't be a problem (or at least not enough of a problem to offset the huge gain in simplicity).
One question I have: In your bugsnag example, suppose I wanted something like this?
{
error : (BugSnagResponse -> msg), Str, Str -> output msg,
warning : (BugSnagResponse -> msg), Str, Str -> output msg,
info : (BugSnagResponse -> msg), Str, Str -> output msg,
}
That is to say, I want to handle the http response from the bugsnag service. I'm pretty sure my example above won't work because all the fields are stuck using the same concrete msg type and also I'm guessing Roc's type system won't allow output msg. How would this be done instead?
@Martin Stewart I believe output and msg are both just type variables (placeholders for whatever you want), so these are interchangeable:
(BugSnagResponse -> output), Str, Str -> output
(BugSnagResponse -> msg), Str, Str -> msg
What do you want msg to look like in practice?
The other possibility if required is another layer of transformations. Just like we have the output of a list of bytes as an http request, we would make a response a list of bytes and you would need a function to turn that into a bug snag message
@Brendan Hansknecht Like this?
result = await bugsnag.info "foo" "bar"
when result is
Err e -> ...
Ok response ->
message = Bugsnag.parse response
...
(Edit: bugsnag.parse --> Bugsnag.parse)
I am more specifically talking about this record:
bugsnag : Bugsnag (Task {} [])
bugsnag =
initBugsnag
{
apiKey: "abcdef1234....",
appVersion: "1.0.0",
releaseStage: "test",
user: Guest,
toOutput: \bytes ->
Http.sendBytes Bugsnag.url bytes
}
Add:
fromResult: \bytes -> bugsnag.parse bytes
Though for something with http requests and response, i would suggest using a richer type than list u8
Probably some basic http types that are easy to transform to/from
@Brendan Hansknecht Do you imagine that fromResult would automatically transform the Ok payload in the (expected) Result output of toOutput? Or would it just be manually callable by an application?
I assume it would be automatically called in other bugsnag related functions, so mostly hidden from the user of the library. I also assume it would handle both the success and failure case.
By expecting output == Result, wouldn't fromResult fail to handle any user applications that use an alternative output type like List Result or something custom?
(sorry if this is an unproductive tangent, btw)
I'm not sure I understand the question. The process i see is:
1) rich bugsnag type
2) bytes (or generic http request type)
3) application code that creates bugsnag request and creates tasks or similar, might even group requests
4) platform sends http request
5) platform receives http response
6) application lambda to process results
7) bytes (or generic http response type)
8) rich bugsnag type (including potential failure)
In your example above, toOutput: a -> b and fromResult: a -> c. How could a platform-agnostic Bugsnag package automatically infer the transformation from b to a in order to chain toOutput with fromResult?
Sorry, yeah, those can't be chained. They depend on platform api
Luckily, if a user wanted to ignore all errors (or whatever other strangeness their toOutput might return), toOutput could simply contain the response parsing step. Essentially, if fromResult's input type were to match toOutput's output type, then fromResult's code could just be injected into the end of toOutput.
Very true
Then it doesn't need to even use a type variable at all. Just specify its expected types.
JanCVanB said:
Martin Stewart I believe
outputandmsgare both just type variables (placeholders for whatever you want), so these are interchangeable:(BugSnagResponse -> output), Str, Str -> output (BugSnagResponse -> msg), Str, Str -> msgWhat do you want
msgto look like in practice?
Sorry I wasn't very clear. I'm thinking in terms of the Elm Architecture where making an http request also means including a parameter (the BugSnagResponse -> msg part) that says how the http response can be converted into a value that gets passed to an update function. This is to say, the update function isn't called via Task.await. Instead the platform calls update after the http request is completed.
Hmm, I don't know Elm so I don't have a feel for that pattern, but... (a) you can customize what the toOutput function returns, (b) I imagine that whatever BugSnagResponse -> msg transformation you want to chain, you could simply add that as a final step of the toOutput function you provide, and (c) you may want to perform error handling at the same level that calls bugsnag.error/warning/info. Does anyone with Elm experience have better insight here?
I like the approach that limits the package registry because it's easier to add stuff later than it is to take stuff away
so if only allowing packages that are platform agnostic becomes a problem, we can then start the work of enabling platform specific packages, vs. platform specific packages being allowed then becoming a problem it'll be impossible to take it away once it's there without significant outrage
One thing of the top of my mind is the elm native code thing that caused quite a bit of drama. Now I'm not arguing one way or the other in that regard, just bringing up an example of how taking things away that people are used to can be quite upsetting even if it's for the best.
I'm wondering about this: Would there be a way to share the platform-specific adapters? It feels like that might be important convenience instead of reimplementing the boilerplate for each usage.
Platform specific adaptors have a few options in my view:
1) as part of the platform
2) as a library that shares types with the platform (will have limitations)
3) add some sort of interface spec that enables depending on platform functions without directly depending on the platform
4) special libraries that depend on platforms
Based on this paragraph from the README, t seems that offering ecosystems of platform-specific packages was, at one point, seen as a feature of Roc:
Each Roc platform gets its own separate package repository, with packages built on top of the API that platform exposes. This means each platform has its own ecosystem where everything is built on top of the same shared set of platform-specific primitives.
I assume that thinking around this changed, but it just stood out to me as relevant here
good point! I've updated the README to remove that paragraph :thumbs_up:
Last updated: Jun 16 2026 at 16:19 UTC