Difficulties with polymorhpism and functor types

Hello, I am trying to implement a base type and set of functions that operate on that type so that I can use the same logic for multiple ‘child’ types which extend the base type. The base and child types need to be plain JS objects of a certain shape that PouchDB expects (e.g. each item has an id field and when deleting an item I need to set the _deleted field to true).

I spend a lot of time writing TypeScript for my job, so I am used to being able to do certain things that seem to be difficult to do in ReScript. Here is some code to show roughly how I would go about accomplishing what I want in TypeScript:

module PouchDB {
  // stub
  export type DB_T<ItemT> = {};

  export function newDB<ItemT>(_name: string): DB_T<ItemT> {
    // stub
    return {}
  }

  export function getAll<ItemT>(_db: DB_T<ItemT>): Array<ItemT> {
    // stub
    return [];
  }

  export function putItem<ItemT>(_db: DB_T<ItemT>, _item: ItemT) {
    // stub
  }
  
}

interface BaseInterface {
  id: string;
  name: string;
  _deleted?: boolean;
  parents?: Array<string>;
}

abstract class BaseModel<ItemT extends BaseInterface> {

  db: PouchDB.DB_T<ItemT>;

  constructor(dbName: string) {
    this.db = PouchDB.newDB(dbName);
  }
  
  getAll() {
    return PouchDB.getAll(this.db)
  }

  get(id: string) {
    return this.getAll().find(item => item.id === id)!;
  }

  add(item: ItemT) {
    return PouchDB.putItem(this.db, item);
  }

  remove(item: ItemT) {
    item._deleted = true;
    return PouchDB.putItem(this.db, item);
  }
}

interface Task extends BaseInterface {
  note?: string;
  due?: Date;
  priority?: number;
  repeat?: boolean;
  tags?: Array<string>;
  dependsOn?: Array<string>;
}

class TasksModel extends BaseModel<Task> {
  constructor() {
    super("tasks");
  }
}

interface Tag extends BaseInterface {}

class TagsModel extends BaseModel<Tag> {
  constructor() {
    super("tags");
  }
}

const tasksModel = new TasksModel();
const tagsModel = new TagsModel();

With all of the above, I am able to do things like the following and the compiler is happy:

tasksModel.add({
  id: "aaa",
  name: "some task"
});

tagsModel.add({
  id: "aab",
  name: "some tag"
});

Here is my attempt so far at doing the same thing in ReScript:

module PouchDB = {
  type t<'item_t> = int

  // stub
  let new = (_name: string): t<'item_t> => 0

  // stub
  let getAll = (_dbr: t<'item_t>): array<'item_t> => []

  let putItem = (_dbr: t<'item_t>, _item: 'item_t) => ()
}

module BaseInterface = {
  type t = {
    id: string,
    name: string,
    _deleted?: bool
  }
  let getId = (item: t) => item.id
  let setDeleted = (item: t) => {...item, _deleted: true}
}

module type MakeModelParamsType = {
  type item_t
  let dbName: string
  let getItemID: item_t => string
  let setDeleted: item_t => item_t
}

module MakeModel = (Params: MakeModelParamsType) =>
{
  type item_t = Params.item_t

  let db: PouchDB.t<Params.item_t> = PouchDB.new(Params.dbName)

  let getAll = () => db->PouchDB.getAll

  let getFrom = (from, id) =>
    from->Array.find(item => item->Params.getItemID === id)->Option.getExn

  let get = id => getAll()->getFrom(id)

  let add = (item: Params.item_t) => db->PouchDB.putItem(item)

  let remove = (item: Params.item_t) =>
    db->PouchDB.putItem(Params.setDeleted(item))
}

module TasksModelParams: MakeModelParamsType = {
  type item_t = {
    ...BaseInterface.t,
    note?: string,
    due?: Js.Date.t,
    priority?: int,
    repeat?: bool,
    tags?: array<string>,
    dependsOn?: array<string>
  }

  let dbName = "tasks"

  let getItemID = (task: item_t) =>
    BaseInterface.getId(task :> BaseInterface.t)

  external castFromBaseT: BaseInterface.t => item_t = "%identity"
  let setDeleted = (task: item_t) =>
    BaseInterface.setDeleted(task :> BaseInterface.t)->castFromBaseT
}
module TasksModel = MakeModel(TasksModelParams)

When I attempt to use the TasksModel module it does not behave the way I would expect:

// Error: The record field id can't be found.
let task: TasksModel.item_t = {id: "aaa", name: "some task"}

// Error: The record field id can't be found.
TasksModel.add({id: "aaa", name: "some task"})

I would like to know if there is a way to expose the TasksModel.item_t type for use outside of the resulting module. I tried with the (ModuleType with type x = y) syntax but couldn’t get the result I wanted with that.

2 Likes

The reason you’re getting that error about The record field id can't be found is because you’ve turned the TasksModelParams.item_t into an abstract type with this line:

module TasksModelParams: MakeModelParamsType = {

because MakeModelParamsType has an abstract type item_t.

Try removing that and see if it works. (It does in rescript v10 at least after I stub out all of the record spread stuff.)


Btw, are you using the rescript 11? If you’re using rescript v10 you won’t be able to spread record definitions or coerce subtypes, like you have in your example. (See this blog post.)


Edit: Here is a playground link to a slightly different organization…feel free to ignore it if you don’t like it! It’s rescript 10 so no record spreads or coercing.

2 Likes

I removed the annotation as you suggested and now everything works perfectly, thanks so much!

I am using the recent ReScript 11 release candidate so the record type spread and coercion works; its really nice to be able to do that without having to resort to ReScript Objects.

And thank you for the example, its super interesting and helpful to see how someone with more ReScript experience would go about structuring this. In particular it hadn’t occurred to me to place modules and module types within modules.

1 Like

That is inspired by how the Base OCaml library is organized. (E.g., Container and Monad modules.) I like that style personally because I come from OCaml and in particular use that standard library.

The cool thing about ReScript is that you can use a style like that if you are already comfortable with it, or you can go with a style that is potentially more appealing for someone more used to JS/TS. It’s nice and flexible in that way!

1 Like

This is a very typical question for users come from ts side.