Stream: ideas

Topic: module params & dependency parametrization


view this post on Zulip Johan Lövgren (Jan 04 2024 at 06:53):

(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.

view this post on Zulip Eli Dowling (Jan 04 2024 at 09:01):

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.

view this post on Zulip Eli Dowling (Jan 04 2024 at 09:03):

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.

view this post on Zulip Johan Lövgren (Jan 04 2024 at 10:11):

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

view this post on Zulip Johan Lövgren (Jan 04 2024 at 10:12):

So that is a concrete suggestion, if that is not already a planned feature of module params

view this post on Zulip Eli Dowling (Jan 04 2024 at 21:41):

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}

view this post on Zulip Eli Dowling (Jan 04 2024 at 21:52):

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:

view this post on Zulip Johan Lövgren (Jan 05 2024 at 06:59):

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!

view this post on Zulip Johan Lövgren (Jan 05 2024 at 07:00):

I think what I was imagining was either

  1. just defining an anonymous record which has the right functions you need, as input to the model.
  2. Defining a third module which just defines the type, and importing this in both the module that needs it and the module that provides it (kind of like defining an interface in OO)

view this post on Zulip Johan Lövgren (Jan 05 2024 at 07:03):

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}

view this post on Zulip Johan Lövgren (Jan 05 2024 at 07:13):

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}

view this post on Zulip Johan Lövgren (Jan 05 2024 at 07:14):

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