"generic" modules

I’m not sure how to “configure” a first-class module using run-time values. Here is a simple example. A quiz can be dynamically configured when it is initialized. Some quizzes might take a configuration parameter for the number/difficulty of questions and other quizzes might be configured by topic. The configuration is “generic” - it differs by quiz. These examples here don’t compile because the compiler is expecting the let initialize to be generic but I’m hardwiring the type. I can get this to work using functors. The FancyQuiz could be built using a functor with a configuration module like let count = 23; let difficulty = "hard" but I want these parameters to be dynamic; the user should be able to type it in.

module type QuizLogic = {
  type t
  let initialize: 'configuration => t
  let questions: t => array<string>
}

module EmptyQuiz: QuizLogic = {
  type t = array<string>
  let initialize = () => []
  let questions = (i: t) => i
}

module FancyQuiz: QuizLogic = {
  type t = array<string>
  let initialize = ((count, difficulty)) =>
    switch difficulty {
    | "hard" => Array.makeBy(count, i => `Hard Question # ${i->Int.toString}`)
    | _ => Array.makeBy(count, i => `Easy Question # ${i->Int.toString}`)
    }
  let questions = (i: t) => i
}

Here is another example of something similar that also won’t compile.

module type MathTypeA = {
  type domain
  let add: (domain, domain) => domain
  let subtract: (domain, domain) => domain
  let zero: domain
}

module IntMath: MathTypeA = {
  type domain = int
  let add = (x, y) => x + y
  let subtract = (x, y) => x - y
  let zero = 0
}

// compile error
let doubleA = (~val: 'x, ~math: module(MathType with type domain := 'x)) => {
  let module(Math) = math
  Math.add(val, val)
}

// using the module with a value that is specific to the IntMath implementation
doubleA(4, moduel(IntMath))

I’m having trouble formulating my question here and getting my head around this. I’m looking for some kind of “generic” module but can’t figure out how to do it. I want the flexibility of first-class modules, where you can swap out one implementation for another, but also want to customize them dynamically rather than using functors where everything has to be statically defined. In the first example above, I tried using a generic parameter in the module type - let initialize - but that didn’t work. In the second example, I tried using an abstract type - type domain - but that didn’t work in my doubleA function.

=== UPDATE ===

Here is something closer to what I’m actually trying to build. I’ve got function giveQuiz that is supposed to take a module matching the QuizLogic type AND a configuration for that particular kind of quiz and then do something with it. This giveQuiz function is flexible because I can plug in any kind of quiz (each quiz can have a different set of configuration properties) and customize the parameters of the quiz by passing different values into the function. This doesn’t compile.

module type QuizLogic = {
  type quiz
  type configuration
  let make: configuration => quiz
  let questions: quiz => array<string>
}

module FancyQuiz: QuizLogic = {
  type quiz = array<string>
  type configuration = {
    difficultyLevel: int,
    repeatQuestionIfWrongAnswer: bool,
  }
  let make = (_config: configuration) => [] // would use the config for this
  let questions = (q: quiz) => q
}

// Syntax error : The type of this packed module contains variables.
let giveQuiz = (config: 'a, quizLogic: module(QuizLogic with type configuration = 'a)) => {
  let module(Q) = quizLogic
  switch Q.make(config)->Q.questions {
  | [] => "No questions!"
  | xs => "The quiz has some questions!"
  }
}
2 Likes

For modules which contain abstract types, when you want to unpack them at runtime, the types can’t appear out of thin air at runtime, so OCaml provides a way to pass in locally abstract types to functions which can then pass them in to modules. Here’s a working example of your quiz code.

2 Likes

Wow! Thank you so much! First, I don’t think I’ve ever seen that syntax where the first parameter of a function is a type and you don’t pass that in when calling the function. Is that documented anywhere?

let giveQuiz = (
  type cfg,
  config: cfg,
  quizLogic: module(QuizLogic with type configuration = cfg),
) => {
  let module(Q) = quizLogic

  switch config->Q.make->Q.questions {
  | [] => "No questions!"
  | _ => "The quiz has some questions!"
  }
}

// Calling the giveQuiz function
let config: FancyQuiz.configuration = {difficultyLevel: 3, repeatQuestionIfWrongAnswer: true}
giveQuiz(config, module(FancyQuiz))->ignore

It reminds me of the syntax in TypeScript for generics on functions like this. So in ReScript rather than doing <A,B> you’d put (type A, type B, ... as the beginning of the function definition. Is that a fair comparision?

function doSomething<A,B>(a: A, b:B): void {
  ...
}

I realized I could accomplish what I wanted by not passing in a first-class module to the giveQuiz function but instead passing in all the functions that I need, like this below. But I prefer passing in the module (rather than a list of functions) and now I know I can do that. Thanks again!

let giveQuiz2 = (config, make, questions) => {
  switch config->make->questions {
  | [] => "No questions!"
  | _ => "The quiz has some questions!"
  }
}

giveQuiz2(config, FancyQuiz.make, FancyQuiz.questions)->ignore
1 Like

Yeah, it’s documented in the OCaml manual. Search for the keywords I mentioned, ‘ocaml locally abstract types’. Should be the first hit.

It reminds me of the syntax in TypeScript for generics on functions… Is that a fair comparision?

In some cases, yes. In ReScript though we would usually want to annotate the module using and interface file or module signature, so usually you wouldn’t need this powerful feature. You’d need locally abstract types when dealing with types that are needed at runtime, as I mentioned earlier. In many cases you wouldn’t, and the signature would look like:

/** Interface file */
let doSomething: ('a, 'b) => unit

I realized I could accomplish what I wanted by… passing in all the functions that I need

Yeah, this is imho much simpler and easier to understand.