"Virtual methods" - not functional or good practice?

Is doing something like virtual methods in ReScript possible or even a good practice? Consider the code below. If you’ve got a generic animal “class” and want to convert it to a string, here is one way to do it. Definitely easy to understand. I just need to do a switch between the known flavors of the object to know how to convert it to a string. This only works for known instances, unlike abstract classes with virtual methods, that can work on any kind. Is what I’ve got here the standard “functional” way of doing things or is there a cleaner way more akin to virtual methods on abstract classes?

 type genericAnimal<'category> = {
    category: 'category,
    name: string,
  }

  type size = Small | Medium | Large
  type kind = Mammal | Bird | Reptile | Insect

  let asText = (a: genericAnimal<'t>, f: 't => string) => a.name ++ "(" ++ f(a.category) ++ ")"

  let sizeText = i =>
    switch i {
    | Small => "small"
    | Medium => "medium"
    | Large => "large"
    }

  let kindText = i =>
    switch i {
    | Mammal => "mammal"
    | Bird => "bird"
    | Insect => "insect"
    | Reptile => "reptile"
    }

  type animal =
    | SizedAnimal(genericAnimal<size>)
    | KindAnimal(genericAnimal<kind>)

  let animalAsText = w =>
    switch w {
    | SizedAnimal(a) => a->asText(sizeText)
    | KindAnimal(a) => a->asText(kindText)
    }

Hi @jmagaram

It’s a good question. I suspect there are different ways to represent abstract/virtual functions in ReScript. I’m not an expert, but I believe you’re on the right track.

There is a little awkwardness with converting the genericAnimal<'category> to a string. Here’s a slight tweak suggestion:

let animalAsText = (a: animal) => {
  let (name, category) = switch a {
  | SizedAnimal({name, category}) => (name, sizeText(category))
  | KindAnimal({name, category}) => (name, kindText(category))
  }
  name ++ "(" ++ category ++ ")"
}

But I believe it depends on your use case.

Lastly, there are a few different ways your original example could have been written using abstract/virtual functions in JS. If you have some example code then it will be easier to provide a suggestion of how it might be written in ReScript.

Hope that helps.

@jmagaram As per your current implementation, category in genericAnimal is polymorphic. So it is possible to directly call and convert the record to text like below.

// Ok
asText({category: Small, name: "Bird"}, sizeText)

// This is wrong, category is `int` ???
asText({category: 1, name: "One"}, _ => "1")

I would restrict and change the implementation like below. But it is just my opinion. This is still functional and good approach.

type size = Small | Medium | Large
type kind = Mammal | Bird | Reptile | Insect
type category = Size(size) | Kind(kind)

type genericAnimal = {
  category: category,
  name: string,
}

let animalAsText = w =>
  switch w {
  | Size(i) =>
    switch i {
    | Small => "small"
    | Medium => "medium"
    | Large => "large"
    }
  | Kind(i) =>
    switch i {
    | Mammal => "mammal"
    | Bird => "bird"
    | Insect => "insect"
    | Reptile => "reptile"
    }
  }

let asText = (a: genericAnimal) => a.name ++ "(" ++ animalAsText(a.category) ++ ")"

// Ok
asText({category: Size(Small), name: "Bird"})->Js.log

// I cannot do this now
asText({category: 1, name: "Bird"})

Thanks for the suggestions. My example is somewhat contrived. But in my real code I do have several things (subtypes) I sometimes want to treat as one thing (the super type or base class). In ReScript it seems necessary to wrap the subtypes in a variant for the base class and then write those switch expressions to figure out which particular method to call on each subtype. This works but is somewhat tedious and is also limited to the known subtypes in my variant base. It doesn’t seem like there is a “dynamic dispatch” capability like in OO languages with abstract classes and virtual methods.

Ok think I just found the answer. This isn’t really supported well in ReScript. The “modular implicits” capability is needed.

1 Like

@jmagaram I think I understand your question better now. I guess you are looking for doing some thing like below. Please let me know if it answers your question. Solution is to use module system in Rescript. Even though this topic is marked as solved I am just adding this solution, so that it could be helpful to somebody in future.

// Consider this as interface
module type Animal = {
  type t
  let speak: unit => string
}

// Lion: Implementation of Animal
module Lion: Animal = {
  type t
  let speak = () => "Roar"
}

// Cat: Implementation of Animal
module Cat: Animal = {
  type t
  let speak = () => "Meow"
}

// A function that takes array of modules of type Animal and calls `speak`
// This function just refers a `module type` and not to a concrete module
let speakAll = (m: array<module(Animal)>) =>
  m->Belt.Array.forEach(a => {
    let module(A) = a
    A.speak()->Js.log
  })

// Pass array of any animal types into speak all
[module(Lion), module(Cat)]->speakAll

Interesting didn’t know you could use modules that way.

1 Like