(See the thread on module params & APIs for more context)
This is an idea for how to architecture larger roc applications using module params. The idea is to use them for dependency parametrization, which is a functional programming variant of dependency injection. Wether this architecture is good is a separate question, but one nice aspect of it is that it is quite declarative: services/modules specify what dependencies they need in order to operate, and then the caller specifies (injects) those dependencies.
In other languages this can be accomplished using partial application, as in this F# example:
let compareTwoStrings (logger:ILogger) str1 str2 =
logger.Debug "compareTwoStrings: Starting"
let result =
if str1 > str2 then
Bigger
else if str1 < str2 then
Smaller
else
Equal
logger.Info (sprintf "compareTwoStrings: result=%A" result)
logger.Debug "compareTwoStrings: Finished"
result
Then some service might use this function as
let comparer = compareTwoStrings myLogger
In Roc we don't have automatic partial application, making this style more difficult to use. Furthermore, the partial application style above does clutter the function definitions quite a lot. But in the future we will have module params!
So a Roc application structured like this might look like
persistence = myPersistence
#...
import ServiceA {persistence, authentication, logging}
import DataService {persistence, dataDictionary}
#...
data = \user -> ServiceB.getData user
Just thought I would share this, though I don't know if it is a good fit for Roc projects in the end. But it does have some of the more generic upsides pointed out in the module params proposal, such as enabling simulation tests and "knowing which effects modules can perform at a glance".
As a side note, at work we use dependency injection heavily, for a web app written in C#. There we make use of injection to be able to construct a separate environment which has alternative services with fake data, used for training purposes.
As I've mentioned in your other posts, the closest analogue I know of is ocaml's module system. It suffers from an issue around the composition of complex hierarchies modules though. In C# you're used to doing DI where you basically say "Gimme class A" and somehow you get it at runtime, these systems have to be much more manual obviously. My memory is a little fuzzy but In ocaml you end up having to specify the full signature of the module you'd like to depend on quite often, because functors break type inference for those module types.
This makes nested modules get painful and complex(there are various posts about it online, This one is a bit abstract but does try to explain the issue https://discuss.ocaml.org/t/functor-trouble-how-to-organize-functor-heavy-code/8913(
However with roc's total type inference most of this pain should be avoided and hopefully it will just involve adding the name to each parent's imports to pass it through.
As a side note, my introduction to FP was via F# and I do think one of its greatest disappointments in an otherwise excellent language is the lack of the ML module system.
Interesting. Yes here I am assuming that it is possible to “nest” modules in the sense that a module can take in a parameter which it then can pass to an imported module:
Module Service: {persistence} -> [getData]
Import SubService {persistence}
getData = SubService.func
So that is a concrete suggestion, if that is not already a planned feature of module params
One aspect I'm unsure of is whether you can say a module depends on another entire module
Say you have a UserManager that stores data in a database and you want to inject that, can you do this?:
# UserManager.roc
module { DBAccess } -> [menu]
updateName =\userId, name ->
#...checks etc
user =DBAccess.getUser(id,name)
DBAccess.writeUser(id,{user&name)
#main.roc
import SQLite {connectionString}
import UserManager {SQLite}
I suppose maybe the syntax that makes sense is:
# UserManager.roc
module { dbAccess } -> [menu]
updateName =\userId, name ->
#...checks etc
user =dbAccess.getUser(id,name)
dbAccess.writeUser(id,{user&name)
#main.roc
import SQLite {connectionString}
import UserManager {dbAccess:SQLite}
I guess this raises the question, could we treat a module's exposes as a record?
Assuming the module params essentially defines a record a module requires when imported could we then put the entire contents of another modules exports into one of those fields as shown above?
But maybe I'm getting lost in the weeds and this just isn't the idiomatic roc way of doing this at all :sweat_smile:
Eli Dowling said:
I guess this raises the question, could we treat a module's exposes as a record?
Assuming the module params essentially defines a record a module requires when imported could we then put the entire contents of another modules exports into one of those fields as shown above?
Yes that's an interesting idea!
I think what I was imagining was either
So for 1:
# UserManager.roc
module { dbAccess } -> [menu]
updateName =\userId, name ->
#...checks etc
user =dbAccess.getUser(id,name)
dbAccess.writeUser(id,{user&name)
#main.roc
import SQLite {connectionString}
dbAccess = { getUser: SQLite.getUser, writeUser: SQLite.writeUser }
import UserManager {dbAccess}
Or for 2:
# DbAccess.roc
module { } -> [DbAccess]
DbAccess : {getUser: #type sig, writeUser: typesig }
# UserManager.roc
module { dbAccess: DbAccess } -> [menu]
import DbAccess from DbAccess
#import the type excplicitly unqualified to avoid having to write DbAcces.DbAccess
updateName =\userId, name ->
#...checks etc
user =dbAccess.getUser(id,name)
dbAccess.writeUser(id,{user&name)
#main.roc
import SQLite {connectionString}
dbAccess = { getUser: SQLite.getUser, writeUser: SQLite.writeUser }
import UserManager {dbAccess}
Though looking at this now, I am not sure if this second approach actually contributes anything beyond an explicit type signature :thinking:
Last updated: Jun 16 2026 at 16:19 UTC