[Major] Verse types are not comparable

Summary

It’s very limiting that the language does not automatically make all types comparable. This behavior disallows creating type based lookup tables. For example, a dynamic Scene Graph component could cache some lazy lookup operation with the result in order to speed up future lookup to O(1) cost.

Please select what you are reporting on:

Verse

What Type of Bug are you experiencing?

Verse

Steps to Reproduce

# Error: 'subtype(component)' cannot be used as the type of map keys because it is not comparable for equality.
Cache: [subtype(component)]component = map {}

Expected Result

Types should be comparable.

Observed Result

Types are not comparable. A workaround using castable implies that the code base needs to know all possible subtypes ahead of time and does not permit creation of dynamic lookup tables.

Platform(s)

UEFN (v37.10)

This feature could be very helpful

1 Like

This should apply to all *_subtype(...) macros as well.

1 Like

This isn’t something we’re going to support in Verse. Types in Verse are just functions. In general, it’s not computable (Rice’s theorem) to tell if two functions are equivalent.

FORT-962485 has been ‘Closed’. This issue won’t be fixed—please see examples of why this may be the case.

What about the other report of casting a castable subtype to another related castable subtype instead only casting values to a target type?

Can you link me to the other report you’re referring to?

Here, it hasn’t received a ticket yet:

I don’t think we’d allow this for the same reasons stated here for testing for equality. But let me double check with some other folks so they can check my logic.

Yeah, I’ve confirmed neither this nor the other post is something we plan to support.

Can you elaborate more on what you’re trying to accomplish? Perhaps there are other ways of achieving a similar thing that we can guide you towards.

@saamyjoon Well, that’s a bit unfortunate. I’m stuck with design ideas for dynamic systems. Here are some use cases.

Use case 1:
I have a services entity which currently has editable references to other strongly typed prefabs. This is not ideal, as I have to extend the services entity every time I add a new service prefab to the mix. The static dependency is tedious to maintain and could also lead to SG related caching issues after for example renaming a field. Therefore the goal is to convert it to a dynamic lookup. And here we start to hit a wall. Imagine a lookup process where some party calls Services.LookUp(some_service). The first operation will be lazy and perform a full search, but it should cache the result somewhere. Currently there seems to be no sane way to create a cache that would allow a O(1) lookup. This ends up that all look operation end up being O(n) in worst case. If such lookups are very frequent, this will waste a lot of time.

Use case 2:
Similar to use case 1 I want to stop using pre-defined enums in order to expose comparable values to the editor. Doing it using classes feels very much wrong to me, as we are creating some form of type pollution as well as potentially slowing things down for the required object instantiation compared to some simple value type like a struct or enum. However this seems to be the current status quo (see tag class). Such value has to transfer a bit of associated information. In this scenario I would to send a pre-configured value (such as a type) over some intermediate component, which performs a lookup operation to find a component that can respond to the given type, and also caches it like in use case 1. Imagine a button that is constrained to a certain vehicle type, an intermediate vehicle spawner service component and a multiple vehicle spawner pool components where each pool has a vehicle type associated to them.

Verse is also lacking the ability to merge multiple constraints such as castable_subtype(a) & concrete_subtype(a). On top of that, if we start mixing in some generics (parametrics), things often fall apart quickly due to restrictions, features being not fully implemented or straight bugs that result to runtime issues as the compiler wasn’t able to catch them. However the most annoying issue one is Can't access a function from a preceding type.

I figured out a tiny workaround, but I generally do not like it.

CanHandle(t: concrete_subtype(a))<decides>: void = {
  r: concrete_subtype(a) = t # Workaround an object with `t`
  R := r {} # Construct a value from `t`
  my_target_a_subtype[R] # Now cast the object to a statically known type
}

While this somewhat works, it’s limited to the target type being locally fully known. I cannot delegate that decision to the editor.

@editable
TargetType: concrete_subtype(a)

CanHandle(t: concrete_subtype(a))<decides>: void = {
  r: concrete_subtype(a) = t 
  R := r {} 
  TargetType[R] # Here we'll hit the next wall
}

TargetType[R] will not compile as the compiler does not allow this, at least not right now. Additionally I just ignored the fact that TargetType is strictly speaking not castable. On top of that @editable still does not play well with things like concrete_subtype (the integration doesn’t seem to have shipped yet).

As you can see, we’re jumping through many hoops, but there’s always some kind of a dead end that prevents us from creating dynamic logic.

I don’t know how verse intends to solve the first use case without native, but that will be only permitted to UE use cases and not UEFN.

As for use case 2, I would really appreciate something like.

my_comparable_struct := struct { 
  SomeField: int = 0 # not editable in this case
  ...
}

# anywhere at global scope
@exposed_to_editor
MyValue_1: my_comparable_struct = my_comparable_struct {
  SomeField := 42
}

@exposed_to_editor
MyValue_2: my_comparable_struct = my_comparable_struct {
  SomeField := 41
}

Then the editor would show MyValue_1 and MyValue_2 as options for the my_comparable_struct type.

One issue with that, is it’s not possible to enforce unique values. If all fields were the same, then both MyValue_1 and MyValue_2 would be equal. This is acceptable, but not always desired.

ID := GetUniqueRuntimeID()

It would be great to have a function that provides a unique integer every time it’s called. However the road block will be converges in this case and as far as I can guess MyValue_1 and MyValue_2 aren’t true global constants, but are instantiated every time they are called.