Rescript advance types - escaping scope

Hello
For some time I’m trying to build an abstraction that would enforce component architecture. I want to mimic a bit of Elm architecture (just a little bit) but I feel like I hit a wall with OCaml/Rescript type system.
Attempt 1
I wrote some abstractions:

module type Sandbox = {
  type model
  type msg
  type props
  let initialModel: model
  let update: (model, msg) => model
  let view: (msg => unit, model, props) => React.element
}

let useSandbox = (sandbox: module(Sandbox))  => {
  let module(Sandbox) = sandbox
  let (model, dispatch) = React.useReducer(Sandbox.update, Sandbox.initialModel)
  Sandbox.view(dispatch, model)
}

It ended up with Sandbox.props constructor trying to escape its scope.
I learned what it means by some OCaml examples but it is a bit unfortunate because it mismatches with what I wanted to do in the first place… and I just wanted to make clear, what kind of argument I expect from the function returned by Sanbox.view.
Attempt 2
So I tried this:

module type Sandbox = {
  type model
  type msg
  let initialModel: model
  let update: (model, msg) => model
  let view: (msg => unit, model, 'props) => React.element
}

let useSandbox = (sandbox: module(Sandbox))  => {
  let module(Sandbox) = sandbox
  let (model, dispatch) = React.useReducer(Sandbox.update, Sandbox.initialModel)
  Sandbox.view(dispatch, model)
}

I just remove the props type constructor and tried generic type… Unfortunately, it works as far as I won’t try to use props.
With view defined this way:

let view = (dispatch, model, props) => {
  <div>
    <h1> {text("Actual " ++ props.prefix ++ string_of_int(model.value))} </h1>
    {button(_ => dispatch(Add), "Add")}
    <button onClick={_ => dispatch(Remove)}> {text("Remove")} </button>
    <button onClick={_ => props.switchPrefix()}> {text("Switch Prefix")} </button>
  </div>
}

I’m getting this error:

Values do not match:
let view: (msg => unit, model, props) => React.element
is not included in
let view: (msg => unit, model, 'props) => React.element

Okay… I’m a bit confused here, I’m passing some concrete type but it wants… generic type?
Attempt 3
I tried a bit more, I get back to declare type props in Sandbox module, switch from simple function to functor:

module MakeSandbox = (Item: Sandbox) => {
  let useSandbox = (props) => {
    let (model, dispatch) = React.useReducer(Item.update, Item.initialModel)
    Item.view(dispatch, model, props)
  }
}

and

@react.component
let make = (~prefix: string, ~switchPrefix: unit => unit) => {
  module Sanbox = Browser.MakeSandbox(T)
  Sanbox.useSandbox({prefix, switchPrefix})
}

I finally achieved my goal (while writing this post :grinning_face_with_smiling_eyes:). Initially, I was thinking about moving components make and makeProps to functor as well but I’m not sure if it’s even possible and I’m happy with the result so far.
But! After I failed in the first 2 attempts now my brain itches me because:

  • I feel like there might solution to the first attempt, but I’m just not able to handle it
  • I don’t understand how to deal (or why I can’t deal) with a second attempt

Are you able to help me explain that? :sweat_smile:

Hi, before we jump into the details of your question, one thing–have you checked out https://github.com/OvermindDL1/bucklescript-tea ? It’s a port of the Elm architecture to ReScript.

2 Likes

I believe one concept you may be missing is “locally abstract types.” They allow you to declare new types that are abstract inside a function but polymorphic to outside code. They’re almost always necessary with first-class modules and GADTs.

This should compile:

module type Sandbox = {
  type model
  type msg
  type props
  let initialModel: model
  let update: (model, msg) => model
  let view: (msg => unit, model, props) => React.element
}

// make props a localy abstract type
let useSandbox:
  type p. (module(Sandbox with type props = p), p) => React.element =
  sandbox => {
    module Sandbox = unpack(sandbox)
    let (model, dispatch) = React.useReducer(
      Sandbox.update,
      Sandbox.initialModel,
    )
    Sandbox.view(dispatch, model)
  }

I’m not aware of good ReScript documentation or examples of that yet, but you can read about it on Real World OCaml.

4 Likes

if you don’t want to fully annotate the expression you can use this syntax:

let useSandbox =
  (type p, sandbox: module(Sandbox with type props = p)) => ...
3 Likes

@johnj thanks a lot for the code snippet, the longer explanation, and the link to the article.
I wasn’t really into OCaml, I came from FE world and Rescript is my choice because it has better FFI than Elm or Purescript, and still allows me to use a huge React ecosystem… but I feel like documentation and examples are tiny on the advanced types… So I found myself happy exploring OCaml :grinning_face_with_smiling_eyes: Good to have the resource explaining it more. The syntax is still exotic for me.
@amiralies thanks for the shorter syntax. Once I fully understand it, it will be probably more practical!
@yawaramin thanks a lot for mentioning bucklescript-tea!
To be more precise I personally like Purescript Halogen more than Elm (because of components and global store), but what I wanted exactly is structuring React app by saying “this goes there, and this goes there”. React hooks are great but people often implement them pretty messy.
Anyway, I like Elm’s syntax for describing dom elements, and it’s cool to see something similar in Rescript. I’m definitely going to try it!

2 Likes