#just short of a heterogeneous dict

1 messages · Page 1 of 1 (latest)

regal iron
#

Hi! First off, I am a Gleam noob. This is my second day of learning. :)

Goal: I want to build an Entity Component System based game engine. In ECS, "components" are arbitrary pieces of data that can be attached to game entities, stored in the game world, and queried. It is the architecture used by Rust's Bevy engine.

Abstract goal: I want to have a central storage location for data that I don't know the type of, but I don't want devs using the library to lose out on any type safety when operating on that storage. Basically I want a heterogeneous/generic dict.

But Gleam doesn't have that. But I made something that comes pretty close by using constructors as keys to a closure-captured dynamic dict.

pub type Position {
  Position
  PositionData(x: Int, y: Int)
}

pub fn main() -> Nil {
  new_world()
  |> add_entity()

  // meh, why do we need two types
  |> component(Position).set(PositionData(1, 2))

  // problem: ideally this would error because we want a `Position` value, not the constructor
  |> component(Position).set(Position)

  // best case scenario (doesn't work because of type mismatch):
  |> component(Position).set(Position(1, 2))

  Nil
}

I guess my question is: is there a way to get the return type of a constructor fn? Like TypeScript's ReturnType. I don't think so? That feels like the last thing blocking me. If it doesn't exist, would it be reasonable to add to the language?

code here, it is short/skimmable: https://github.com/willmartian/glecs/blob/main/src/example.gleam#L16-L22

Thank y'all in advance! (Also, Gleam is really fun!)

GitHub

ECS game library for Gleam. Contribute to willmartian/glecs development by creating an account on GitHub.

foggy plume
#

Would it not be possible to use strings for component keys instead of the type constructor?

#

Cause types don't exist at runtime, only type constructors, and there's no way using generics to make a function that allows any type constructor to be passed in, because they can have different arity

#

When you have a type like

type Position {
  Position(x: Int, y: Int)
}

And write

component(Position)

That Position isn't referring to the type name (line 1 of first example), it's referring to the constructor (line 2 of first example), they just happen to share the same name in that situation but they're part of completely separate namespaces, the type namespace and the value namespace. Only the things in the value namespace exists after compilation.
To make it more clear, if you instead wrote

type Foo {
  Bar(x: Int, y: Int)
}

Then you can use Bar as a value and pass it around (it will be a function that accepts two ints and returns a record of type Foo), but you can't use Foo as a value, since that can only be used in type signatures.

#

So even if you had a ReturnType helper, I don't see how that would help you here, because that still wouldn't let you write a function that accepted any constructor along with an instance of that value

fn foo(c: cons, val: ReturnType(cons)) { ... }

In that example above cons could be any type, including things that aren't constructors, so how would ReturnType handle that? If you tried to make the constructor's type more specific then you run into the issue of different constructors having different numbers of args:

fn foo(c: fn(x: a, ???) -> t, val: t) { ... }
hardy eagle
#

The constructors are not be stable keys so you could have multiple Position entries in that dictionary with this design

#

The code is doing unsafe type casts, which is not ideal

regal iron
#

Ty both for your replies! @hardy eagle @foggy plume

Would it not be possible to use strings for component keys instead of the type constructor?

The code is doing unsafe type casts, which is not ideal

My thinking was: if we use constructors as keys, it allows us to enforce type safety during setting+getting. Unsafe type casts feel less problematic to me if they are use behind a safe interface.

The constructors are not be stable keys so you could have multiple Position entries in that dictionary with this design

Would you consider that a bug? When you say they are not stable keys, does that mean that the equality check is not stable? (Lmk if I am missing something)

pub type FooBar{
  Foo()
  Bar()
}

pub fn main() {
  echo Foo == Foo // --> true
  echo Bar == Bar // --> true
  echo Foo == Bar // --> false
}

So even if you had a ReturnType helper, I don't see how that would help you here, because that still wouldn't let you write a function that accepted any constructor along with an instance of that value

In that example above cons could be any type, including things that aren't constructors, so how would ReturnType handle that?
The first thing I think of is making the ReturnType-like helper be able to operate on any type. If the type is callable, it returns the return type. If it is not callable, it returns the source type. Maybe a better name would be ResolvedType or similar.

Note: I have no experience in language design, and most of my experience is with Typescript, which is fairly different from Gleam! But this approach just seems so close to being usable--are the aforementioned blockers worth solving to facilitate something like this?

hardy eagle
#

It’s not a bug, no. There’s no guarantees about function pointer uniqueness or reuse.

#

I don’t agree that this approach is close to being usable. It has the bugs mentioned, it abuses the type system to try and make a non-functional API in a functional language, and it is doesn’t actually bring the performance benefits one would expect from a DOD data model

#

I would encourage using functional design patterns instead

regal iron
#

Understood! I will chew on that some and research other patterns 🫡

stiff atlas
#

I could recommend this (very long) talk from Casey Muratori at the Better Software Conference. https://youtu.be/wo84LFzx5nI

Part of it covers what is possibly the first implementation of ECS in a game (Thief: The Dark Project - a favourite of mine) except it was never named that.

Entities were kept in structs, so the same data for each entity. Some of the fields are not used for every entity type. Extra behaviors were given using tags.

It sounds like you need behaviours that require additional data. You may benefit from each entity having a field that is a list of behaviour data. One type with multiple variants.

The video is good. It may be long but there is no fat on it.

regal iron
#

Thanks so much @stiff atlas ! 🙇