Stream: beginners

Topic: ✔ Roc's prefered approach to dynamic dispatch via Interfaces


view this post on Zulip Gladisane (Aug 27 2024 at 11:36):

Hi, I'm just toying around with Roc to explore functional programming. Coming from the OOP (Java) world having some (limited) experience with Rust, I found a common pattern such that a library declares an interface (or trait + trait object in case of Rust) and lets clients implement methods declared by it. Then there can be a collection of type of said Interface, a method for client to add to the collection, and finally the library does dynamic dispatch on each element of said collection. Searching through chat history, I saw this has come up, but the Roc-blessed approach (somewhere someone suggested a record of functions) is still not very clear to me.
For example, in Java I can have an interface of

public interface Renderable {
    public String render();
}

and clients can implement classes of Person, Animal, Object with their own implementations. The library can then do:

public class Scene {
    ArrayList<Renderable> things = new ArrayList<>();
    // constructors an then a method to add things
   .....
   // Scene rendering
  public void render() {
    for (Renderable thing: things) {
      System.out.println(thing.render());
    }
  }
}

The closest to this in Roc I've found is to declare a tagged union of Renderable : [Person PersonData, Object ObjectData, Animal AnimalData], then a render : Renderable -> Str that redirects the XData records to its correct rendering implementation. This of course would not let any client implement their own Renderable types at all as they would need to modify both the Renderable union and the render redirecting function.
So what is the approach that Roc (and other functional programming languages in general) takes?

view this post on Zulip Anton (Aug 27 2024 at 11:42):

Hi @Gladisane, I think abilities can do what you are looking for.

view this post on Zulip Gladisane (Aug 27 2024 at 11:53):

Anton said:

Hi Gladisane, I think abilities can do what you are looking for.

To my understanding, Abilities is a compile-time concept only and is highly discouraged for custom use. Roc also disallows implementing new Abilities for existing types, so you can't have a game engine that exposes a Renderable and asks users to implement it. Do I have any conceptual misunderstanding here?

view this post on Zulip Anton (Aug 27 2024 at 12:35):

That's all correct I believe. I've thought about translating inheritance to Roc before, I then wrote up an issue to make an example for it :)

Anyway, I have basically no game engine experience but it seems like you could work with a general function that accepts two arguments like Vertices and Texture. A Person, Animal and Object would all just require Vertices and a Texture to render right? Let's leave shaders out of this for now :p that should not alter the fundamentals here.

view this post on Zulip Anton (Aug 27 2024 at 12:37):

Any user of your API (game engine platform) could provide Vertices and a Texture to that function for whatever custom thing they want in the game.

view this post on Zulip Anton (Aug 27 2024 at 13:09):

Contemplating on it more, I think use of custom abilities could be justified here to keep things conceptually simple. I do think it would not be recommended if you want maximum performance, but you could go with something like this:

Renderable implements
    render : entity -> (Vertices, Texture) where entity implements Renderable

view this post on Zulip Anton (Aug 27 2024 at 13:11):

There would be no need for the user to alter/extend (Vertices, Texture) because that is all the game engine needs to have in order to render something.

view this post on Zulip Gladisane (Aug 27 2024 at 15:59):

I don't think that would work because, I assume, abilities—unlike Java interfaces or Rust trait object—will get compiled down to a single type at compile-time. That means the collection List Renderable can only hold one type only, appending another type (e.g adding Person after Object) will get compiler type mismatch error.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:29):

Yeah, an ability is not a solution to dynamic dispatch.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:29):

That said, if you can fit into an ability and it might make the most sense in some cases similar to this

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:30):

I would say that the first suggestions is definitely switching to a form of compositions with tagged data.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:30):

So you would just use a when ... is to do the dispatch.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:31):

Roc theoretically could support boxed dyn abilities like Rust supports boxed dyn traits, but that is not a feature today and is not currently planned.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:32):

Fundamentally, if you are missing multiple different types into the same data structure, roc requires tagging.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:34):

One final note, for functions called on a single element. You can make then have dynamic dispatch equivalent by passing function into the function.

Render : a, (a -> Vertex), (a -> Texture) -> Task {} [FailedToRender]

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:35):

That enable essentially building up your dynamic dispatch table at runtime. You could create a record containing all of the functions that the data might need.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 16:36):

That said, if possible, I think it is generally preferred to use composition and have functions take a subset of the data.

view this post on Zulip Richard Feldman (Aug 27 2024 at 18:49):

welcome @Gladisane! :wave:

Gladisane said:

Hi, I'm just toying around with Roc to explore functional programming. Coming from the OOP (Java) world having some (limited) experience with Rust, I found a common pattern such that a library declares an interface (or trait + trait object in case of Rust) and lets clients implement methods declared by it.

[...]

So what is the approach that Roc (and other functional programming languages in general) takes?

I can't speak for other languages, but I would say that the approach that both Roc and Elm take is recommending against organizing code this way :big_smile:

view this post on Zulip Richard Feldman (Aug 27 2024 at 18:50):

I spent a lot of time writing code like this, and also writing code in a "just don't bother doing that" style and my conclusion is that doing it this way is the wrong default way to organize code in general (not just in FP, but in OOP too - which I realize is controversial depending on who you ask!)

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 18:50):

Also, to be concrete on how to make a user extendable type that gets put in a list, I think you would have to do something like this:

Renderable a: [Person PersonData, Object ObjectData, Animal AnimalData, UserType a]

renderAll : List (Renderable a), (a -> Str) -> Str

Which fells pretty eh.

Edit: This can be a bit nicer with an ability, still discouraged, but better

Render implements
    renderEntity : a -> Str

Renderable a: [Person PersonData, Object ObjectData, Animal AnimalData, UserType a]
    where a implement Render

renderAll : List (Renderable a) -> Str

Then the users can implement render on there type and just stick it in UserType. They would aggregate all of their possible types into the single tag. The code would be able to store all Renderables into the single list.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 18:52):

That or force the user to define the render function.

view this post on Zulip Brendan Hansknecht (Aug 27 2024 at 18:52):

So I don't know of a good solution to that specific problem apart from not modeling code that way.

view this post on Zulip Richard Feldman (Aug 27 2024 at 18:53):

Gladisane said:

Anton said:

Hi Gladisane, I think abilities can do what you are looking for.

To my understanding, Abilities is a compile-time concept only and is highly discouraged for custom use. Roc also disallows implementing new Abilities for existing types, so you can't have a game engine that exposes a Renderable and asks users to implement it. Do I have any conceptual misunderstanding here?

this is all totally correct, and in fact one of the main reasons I wanted to explicitly and strongly discourage reaching for abilities as a matter of course is that I believe it leads to overcomplicated code compared to just not doing that, and one of my fears with introducing abilities to the language is that it would lead to code that's overcomplicated because I know it's culturally encouraged to write code in this way in OOP communities :big_smile:

view this post on Zulip Richard Feldman (Aug 27 2024 at 19:00):

a game engine that exposes a Renderable and asks users to implement it

I don't have an exact example of how a game engine in Roc would look, but here's an example of a UI framework design for Roc - so it has the same concept of a rendering system which exposes ways for users of the system to specify how they want things to be rendered

view this post on Zulip Richard Feldman (Aug 27 2024 at 19:08):

without going too far down the rabbit hole, one of the key differences is that:

view this post on Zulip Gladisane (Aug 28 2024 at 08:40):

Richard Feldman said:

I spent a lot of time writing code like this, and also writing code in a "just don't bother doing that" style and my conclusion is that doing it this way is the wrong default way to organize code in general (not just in FP, but in OOP too - which I realize is controversial depending on who you ask!)

Well, I guess it's another case of reconditioning away from ingrained OOP patterns for me.

Brendan Hansknecht said:

That or force the user to define the render function.

That's probably the right way to go in the FP world, right, just embracing first-class functions.

BTW, what is the syntax for function that takes no argument/thunking in Roc. It seems a function type of (* -> Str) would not work, but after some tinkering ({} -> Str) seems to be the right one, but it still requires passing in {} as an argument for function call.

view this post on Zulip Luke Boswell (Aug 28 2024 at 08:53):

Yeah {} -> Str is the way we do a thunk in roc. You still pass in an empty record argument to call the function.

view this post on Zulip Gladisane (Aug 28 2024 at 14:45):

Thanks for all the responses, this is really helpful!

view this post on Zulip Notification Bot (Aug 28 2024 at 14:46):

Gladisane has marked this topic as resolved.


Last updated: Jul 06 2025 at 12:14 UTC