vim / vim

The official Vim repository
https://www.vim.org
Vim License
36.63k stars 5.46k forks source link

[vim9class] enum_value->string() issues #14330

Closed errael closed 7 months ago

errael commented 7 months ago

Steps to reproduce

string(enum_value) no longer includes variable values.

Expected behaviour

@zzzyxwvut @yegappan

A class', and initially an enum's, default string() method provides useful debug information by including instance variables in the string return. If wanted it can be overridden to something more useful to the programmer.

In a recent change to enum classes, the default string() method now returns something of the form "Planet.earth" rather than output something like a class. I believe this change was made related to

class variables leak into "values".

in comment https://github.com/vim/vim/pull/14224#issuecomment-2023939547, if values refers to an enum's static values, like list<Planet>, then the statement doesn't make sense. string(obj) has always included class variables.

The change to string(enum_value) removes some useful functionality: the display of instance contents. It also diverges enum from class.

There is also, in https://github.com/vim/vim/pull/14224#issuecomment-2023172587

Suppose I want to store enumeration values as dictionary
keys, and use string() for them. How can I later undo
that conversion, as I get E121 with eval(string(Foo.BAR))?

I didn't fully understand this on first read. Looking again, it seems that the idea is to be able to use both "Planet.earth" and "Count.ONE" as keys in a dict and reconstruct the original enum value using eval(key).

The change to enum's string() also handles this, but breaks expected usage of string() for a class.

Here's a suggestion to facilitate constructing a dict key for an enum. Add an instance variable class_name to an enum value. Then the developer can do

    var e = Planet.earth
    some_dict[e.class_name .. '.' .. e.name] = something

A similar possibility would be adding a builtin method/var AsKey()/as_key to the enum instance; then e.as_key->split('.')[0] is Planet.

Note that any of these changes to string() for specific purposes, like generating a dict key, seems to interfere/conflict with the idea of being able to override string()a.

If class_name isn't needed/wanted, then consider something specific to enums.

Version of Vim

9.1.225

Environment

ubuntu/gtk

Logs and stack traces

No response

yegappan commented 7 months ago

For regular objects, the string() function returns the instance variables and their values. You cannot use eval() to convert the returned string back to the corresponding object. This is different from the return value of other composite objects like Lists and Dicts. I am thinking whether to align the return value of enum to be like Lists and Dicts (current behavior) or to align it with the return value for regular objects. I agree with your suggestions. I will create a PR to change the return value of string() for enums to be similar to that of regular objects and introduce a new enum_name instance variable.

zzzyxwvut commented 7 months ago

I will create a PR to change the return value of string() for enums to be similar to that of regular objects and introduce a new enum_name instance variable.

Do we really need enum_name? Can't we bring back what
string(Foo.BAR) used to produce (i.e. debug information)
so it can be aligned, if desired, to what some string(obj)
would produce? E.g.:

object of Count {count: 1, name: 'one'}
enum Count.ONE {ordinal: 0, name: 'ONE'}

And when there is need to make it terse for key lookups,
have string() overridden (:help builtin-object-methods)?
I did mention dictionary keys as an example, but there was
no suggestion to change textual representation for :enums.
And when I saw the change, it clicked, okay, its shortness
and uniqueness should fare well for key lookups so there is
no point in overriding string().

But can we keep the current ability of eval(string(Foo.BAR))
intact and provide similar deserialisation for objects with
eval(string(obj))?

zzzyxwvut commented 7 months ago

@errael, you need several pre-feature-merge Vim builds to
see the ‘leaky’ problem. If you source the linked example,
these lines are echoed:

(without this.name):

16: enum Count.ONE {ordinal: 0, zero: 0}
16: enum Count.TWO {ordinal: 1, zero: 0}

(with this.name (pay attention to v:numbermax)):

16: enum Count.ONE {name: ONE, ordinal: 0, zero: 0}
16: enum Count.TWO {name: TWO, ordinal: 1, zero: 0}
 0: 9223372036854775807

(with eval(string(Foo.BAR)) (v9.1.0219)):

16: Count.ONE
16: Count.TWO
zzzyxwvut commented 7 months ago

Do you know what would happen to keys of a dictionary for
which enumeration values, wrapped in a string, are used,
after permitted changes have been made to some of these
values?

Try it with a pre-eval(string(Foo.BAR)) build and any later
one (e.g. v9.1.0219).

vim9script

enum Count
    ONE(10), TWO(11)

    var offset: number

    def new(offset: number)
    # Safe, enumerations are not extendable.
    this.SetOffset(offset)
    enddef

    def SetOffset(offset: number)
    this.offset = offset
    enddef

    def ToString(): string
    try
        # This eval(...) roundabout should work for enumerations
        # with either "this.name" available or not (in this case
        # "E121: Undefined variable: enum" is raised for
        # eval('enum Count...')).
        return printf("enum Count.%s {ordinal: %d, offset: %d}",
                eval(string(this) .. '.name'),
                this.ordinal,
                this.offset)
    catch /\<E121:/
    endtry

    return string(this)
    enddef
endenum

var counts: dict<Count> = {}

for value in Count.values
    counts[string(value)] = value
endfor

echo Count.ONE.ToString()
echo counts[string(Count.ONE)].offset
echo Count.TWO.ToString()
echo counts[string(Count.TWO)].offset

echo 'Mutating Count.ONE ...'

Count.ONE.SetOffset(100)
echo Count.ONE.ToString()

try
    try
    # BEFORE: panic at look-up.
    # CURRENTLY: carry on.
    echo counts[string(Count.ONE)].offset
    echo Count.TWO.ToString()
    echo counts[string(Count.TWO)].offset
    finish
    catch /\<E716:/
    throw 'Remove the OLD key, then add a NEW key!'
    endtry
catch /Remove the OLD key, then add a NEW key!/
    try
    try
        echo 'Oops!  Sorry about that.'

        for value in Count.values
        echo printf("%s: %s", string(value), value)
        endfor

        remove(counts, string(Count.ONE))
    catch /\<E716:/
        throw 'Whaaaat?  Shut uuuup!'
    endtry
    catch /Whaaaat?  Shut uuuup!/
    echo 'Ugh!'
    var counts_: dict<Count> = {}

    for value in Count.values
        counts_[string(value)] = value
    endfor

    counts = null_dict
    counts = counts_
    echo counts[string(Count.ONE)].offset
    echo Count.TWO.ToString()
    echo counts[string(Count.TWO)].offset
    finish
    endtry
endtry

Since there is an understanding that textual representation
for enumerations should be changed, it seems futile to file
a bug against its current version.

errael commented 7 months ago

I will create a PR to change the return value of string() for enums to be similar to that of regular objects and introduce a new enum_name instance variable.

Do we really need enum_name? ... And when there is need to make it terse for key lookups, have string() overridden (:help builtin-object-methods)?

But can we keep the current ability of eval(string(Foo.BAR)) intact

AFAICT, it's been possible to override string() for a while so as long as eval("Foo.BAR") works there's no problem.

Personally, I don't like overriding string() to create a key. string() has a variety of uses and creating a key for an enum seems a common thing to do, so there would be interference. So I would probably do something like

vim9script

var d: dict<any>

enum Planet
    earth
    const as_key = 'Planet.' .. this.name
endenum

var x = Planet.earth
d[x.as_key] = 17
echo d

which is not much more work than doing

    const as_key = this.enum_name .. '.' .. this.name

The main difference being that if you change the name of the enum, you don't have to remember to change the as_key initialization. I haven't considered enough if there are other uses of the enum name, such that I'd end up creating that manually. It is a convenience that can be generally/manually provided like an interface, not required, for example

vim9script

interface ClassName
    var class_name: string
endinterface

enum Planet implements ClassName
    earth
    const class_name = "Planet"
endenum

echo Planet.earth.class_name

Since it comes from an interface, there's an easy test to see if it's available.

In Java you can do e.getClass().getName(), not sure about other languages.

and provide similar deserialisation for objects with eval(string(obj))?

IIUC, by this you mean recovering the original object from a string. I think this would require additional support from vim builtins, like obj->as_string() where as string returns something like "C#uniq_id" where "C" is the class name and uniq_id is it's address (can't use address if it might move after a grow). And there needs to be a string->as_object() for going the other way.

I'm about to post something about cloning an object that seems loosely related.

errael commented 7 months ago

you need several pre-feature-merge Vim builds to see the ‘leaky’ problem.

Thanks, I missed the word "class" in your description "class variables leak".

errael commented 7 months ago

Do you know what would happen to keys of a dictionary for which enumeration values, wrapped in a string, are used, after permitted changes have been made to some of these values?

This is the same thing that happens if you try to use a regular class object as a dict index.

zzzyxwvut commented 7 months ago

IIUC, by this you mean recovering the original object from a string.

But nothing beyond what is currently possible for lists,
dictionaries, and enumerations, and within the current Vim
process.

vim9script

const list: list<number> = range(4)
const list_s: string = string(list)
echo (eval(list_s) == list) (eval(list_s) is list)

const dict: dict<blob> = {a: 0z0a, b: 0z0b}
const dict_s: string = string(dict)
echo (eval(dict_s) == dict) (eval(dict_s) is dict)

enum Count
    ONE, TWO
endenum

const enum_one_s: string = string(Count.ONE)
echo (eval(enum_one_s) == Count.ONE) (eval(enum_one_s) is Count.ONE)

class Unity
    const one: number = 1
endclass

const class_one = Unity.new()
const class_one_s: string = string(class_one)
# E121
echo (eval(class_one_s) == class_one) (eval(class_one_s) is class_one)
errael commented 7 months ago

IIUC, by this you mean recovering the original object from a string.

But nothing beyond what is currently possible for lists

I specifically meant recovering the "original" object. As you show, is is false, recovering the original object is beyond what is currently possible. (with enums you always get the original object).

If you don't care about recovering the original object, only something that is ==, that could be done by implementing a clone function on your object. For example,

vim9script

class C
    var x: number
    def Clone(): C
        return C.new(this.x)
    enddef
endclass

var o1 = C.new(17)
echo o1
var o2 = o1.Clone()
echo o2
echo o1 == o2

outputs

object of C {x: 17}
object of C {x: 17}
true

If you really want eval(string(obj)) to work, eval would have to take some arbitrary string and decide if it looks like the output of some random string(obj) output and create an object from that; that seems unreasonable (if not impossible) and beyond what eval() does. Still looks like a new builtin, obj_from_string(string(obj)), makes more sense; and make sure you don't override string().

errael commented 7 months ago

enums to be similar to that of regular objects and introduce a new enum_name instance variable

If you want to implement a clone technique using eval(), it's tricky since eval doesn't have function context. And if there's some generality, you could want the name of the class that the object is an instance of. So something like like class_name could be handy, not sure why you'd want class_name and enum_name both.

errael commented 7 months ago

dict key for class/enum should contain <SID>

I wasn't thinking about dict keys in the entirety of the issue.

Note: these issues apply to both class object and enum value used as a dict key.

Want the dict key unique, meaning is not ==.

If there's a file /tmp/planets with

vim9script

export enum Planet
    earth
endenum

And this file is imported in

vim9script

import '/tmp/planets.vim'

enum Planet
    earth
endenum

echo planets.Planet.earth
echo Planet.earth

There return from string() may change, but the above points out the issue.

The following code is to explore how an object/value can be used as a key. This seems like it would work. And using the '' might also facilitate recovering the object/value. Something similar works for an enums value.

vim9script

# Return something uniq per object in a class.
def GetUniqId(obj: any): string
    return '17'
enddef

class MyClass
    const object_id = GetUniqId(this)
    const class_name = expand('<SID>') .. 'MyClass'
    const as_key = this.class_name .. '@' .. this.object_id
endclass

var o = MyClass.new()
echo o.as_key

outputs something like

<SNR>36_MyClass@17

For an enum, GetUniqId(obj) could just be the enum name. For an object, not sure what works best. Could be a vim internal index into an array of object of given class.

The question is what class/enum constants used to uniquely identify an object/value should be automatically provided by vim.

errael commented 7 months ago

Here's a hack for recovering the object from the key. It hides the original object in the dict value.

vim9script

interface HasAsKey
    var as_key: any
endinterface

def MyPut(arg_dict: dict<any>, key: HasAsKey, val: any)
    arg_dict[key.as_key] = [ key, val ]
enddef

def MyGet(arg_dict: dict<any>, key: HasAsKey): any
    return arg_dict[key.as_key][1]
enddef

def KeyToObj(arg_dict: dict<any>, key: HasAsKey): any
    return arg_dict[key.as_key][0]
enddef

class C implements HasAsKey
    static var _obj_id_count = 1
    final as_key: number
    def new()
        this.as_key = _obj_id_count
        _obj_id_count += 1
    enddef
endclass

var d: dict<any>

var o1 = C.new()
var o2 = C.new()

MyPut(d, o1, 'one')
MyPut(d, o2, 'two')

echo MyGet(d, o1)
echo MyGet(d, o2)

echo KeyToObj(d, o1)
echo KeyToObj(d, o2)

echo d->keys()
echo d

outputs

one
two
object of C {as_key: 1}
object of C {as_key: 2}
['1', '2']
{'1': [object of C {as_key: 1}, 'one'], '2': [object of C {as_key: 2}, 'two']}
errael commented 7 months ago

Note, in https://github.com/vim/vim/issues/14330#issuecomment-2028345962, there an example that suggests a dict key that looks something like <SNR>36_MyClass@17 and a way to turn that back into an object; this needs some system support for generating the @17 unique_id and getting the object back.

There an additional issue that was missed. Since the key is simply a string, the key is not sufficient to prevent the original object from being garbage collected. So a scheme like the hack in https://github.com/vim/vim/issues/14330#issuecomment-2028442761 is required to prevent the object from being garbage collected.

zzzyxwvut commented 7 months ago

As long as you take care of the keys, there are no problems.
The toy Set example below works in v9.1.0261.

File adt.vim:

vim9script

export class Set
    final _elements: dict<number>
    const _Mapper: func(number, string): any
    const ToStringer: func(any): string
    const FromStringer: func(string): any

    static def _Mapper(F: func(string): any): func(number, string): any
    return ((G: func(string): any) => (_: number, v: string): any => G(v))(F)
    enddef

    def new()
    this._elements = {}
    this._Mapper = _Mapper((s: string): any => eval(s))
    this.ToStringer = (a: any): string => string(a)
    this.FromStringer = (s: string): any => eval(s)
    enddef

    def newFromList(elements: list<any>, ToStringer: func(any): string,
                    FromStringer: func(string): any)
    this._elements = elements
        ->reduce(((F: func(any): string) => (d: dict<number>, v: any) =>
        extend({[F(v)]: 1}, d))(ToStringer),
        {})
    this._Mapper = _Mapper(FromStringer)
    this.ToStringer = ToStringer
    this.FromStringer = FromStringer
    enddef

    def _FromList(elements: list<any>): Set
    return Set.newFromList(elements, this.ToStringer, this.FromStringer)
    enddef

    def Contains(element: any): bool
    return has_key(this._elements, this.ToStringer(element))
    enddef

    def Elements(): list<any>
    return keys(this._elements)->mapnew(this._Mapper)
    enddef

    def empty(): bool
    return empty(this._elements)
    enddef

    def len(): number
    return len(this._elements)
    enddef

    def string(): string
    return string(keys(this._elements))
    enddef

    # {1, 2, 3} ⊇ {1, 2}.
    def Superset(that: Set): bool
    return (len(this._elements) >= len(that._elements)) && that._elements
        ->keys()
        ->indexof(((set: Set) => (_: number, v: string) => !set._elements
        ->has_key(v))(this)) < 0
    enddef

    # {1, 2} ⊆ {1, 2, 3}.
    def Subset(that: Set): bool
    return (len(this._elements) <= len(that._elements)) && this._elements
        ->keys()
        ->indexof(((set: Set) => (_: number, v: string) => !set._elements
        ->has_key(v))(that)) < 0
    enddef

    # {1, 2, 3} ∪ {2, 3, 4} = {1, 2, 3, 4}.
    def Union(that: Set): Set
    return this._FromList({}
        ->extend(that._elements)
        ->extend(this._elements)
        ->keys()
        ->map(this._Mapper))
    enddef

    # {1, 2, 3} ∩ {2, 3, 4} = {2, 3}.
    def Intersection(that: Set): Set
    return this._FromList(this._elements
        ->keys()
        ->filter(((set: Set) => (_: number, v: string) => set._elements
        ->has_key(v))(that))
        ->map(this._Mapper))
    enddef

    # {1, 2, 3} \ {2, 3, 4} = {1}.
    # {2, 3, 4} \ {1, 2, 3} = {4}.
    def SetDifference(that: Set): Set
    return this._FromList(this._elements
        ->keys()
        ->filter(((set: Set) => (_: number, v: string) => !set._elements
        ->has_key(v))(that))
        ->map(this._Mapper))
    enddef

    # {1, 2, 3} △ {2, 3, 4} = {1, 4}.
    def SymmetricDifference(that: Set): Set
    return this.Union(that).SetDifference(this.Intersection(that))
    enddef
endclass

############################################################

if !exists("g:adt_tests")
    finish
endif

############################################################

const ToStr: func(number): string = (s: number) => string(s)
const FromStr: func(string): number = (s: string) => str2nr(s)

echo Set.newFromList([1, 2, 3], ToStr, FromStr)
    .Subset(Set.newFromList([1, 2], ToStr, FromStr))
echo Set.newFromList([1, 2], ToStr, FromStr)
    .Subset(Set.newFromList([1, 2, 3], ToStr, FromStr))
echo Set.newFromList([1, 2], ToStr, FromStr)
    .Superset(Set.newFromList([1, 2, 3], ToStr, FromStr))
echo Set.newFromList([1, 2, 3], ToStr, FromStr)
    .Superset(Set.newFromList([1, 2], ToStr, FromStr))

echo Set.newFromList([1, 2, 3], ToStr, FromStr)
    .Union(Set.newFromList([2, 3, 4], ToStr, FromStr)).Elements()
echo Set.newFromList([2, 3, 4], ToStr, FromStr)
    .Union(Set.newFromList([1, 2, 3], ToStr, FromStr)).Elements()

echo Set.newFromList([1, 2, 3], ToStr, FromStr)
    .Intersection(Set.newFromList([2, 3, 4], ToStr, FromStr)).Elements()
echo Set.newFromList([2, 3, 4], ToStr, FromStr)
    .Intersection(Set.newFromList([1, 2, 3], ToStr, FromStr)).Elements()

echo Set.newFromList([1, 2, 3], ToStr, FromStr)
    .SetDifference(Set.newFromList([2, 3, 4], ToStr, FromStr)).Elements()
echo Set.newFromList([2, 3, 4], ToStr, FromStr)
    .SetDifference(Set.newFromList([1, 2, 3], ToStr, FromStr)).Elements()

echo Set.newFromList([1, 2, 3], ToStr, FromStr)
    .SymmetricDifference(Set.newFromList([2, 3, 4], ToStr, FromStr)).Elements()
echo Set.newFromList([2, 3, 4], ToStr, FromStr)
    .SymmetricDifference(Set.newFromList([1, 2, 3], ToStr, FromStr)).Elements()

############################################################

const none: Set = Set.new()
echo none.len()
echo none.empty()
echo none.string()
echo string(none.Elements())

const sets: Set = Set.newFromList(
    [Set.new(), Set.new(), Set.new(), Set.new()],
    (o: any): string => string(o),
    (s: string): any => eval(s))
echo sets.len()
echo sets.empty()
echo sets.string()
echo string(sets.Elements())

const lists: Set = Set.newFromList(
    [[[[[]]]]],
    (o: any): string => string(o),
    (s: string): any => eval(s))
echo lists.len()
echo lists.empty()
echo lists.string()
echo string(lists.Elements())

File count.vim:

vim9script

import './adt.vim'

interface Keyable
    def AsKey(): string
endinterface

enum Count implements Keyable
    ONE(10), TWO(11)

    var offset: number

    def new(offset: number)
    # Safe, enumerations are not extendable.
    this.SetOffset(offset)
    enddef

    def SetOffset(offset: number)
    this.offset = offset
    enddef

    def AsKey(): string
    return 'Count.' .. this.name
    enddef
endenum

############################################################

const counts: adt.Set = adt.Set.newFromList(
    Count.values,
    (c: Count): string => c.AsKey(),
    (s: string): Count => eval(s))
echo counts.len()
echo counts.empty()
echo counts.Contains(Count.ONE)
echo counts.Contains(Count.TWO)
echo counts.string()
echo string(counts.Elements())

const originals: list<Count> = deepcopy(Count.values, 1)

echo "Mutating Count.ONE ..."
Count.ONE.SetOffset(100)

for element in counts.Elements()
    if index(originals, element) < 0
    throw 'Illegal state'
    endif
endfor

echo counts.len()
echo counts.empty()
echo counts.Contains(Count.ONE)
echo counts.Contains(Count.TWO)
echo counts.string()
echo string(counts.Elements())
errael commented 7 months ago

Basic issue taken care of by https://github.com/vim/vim/commit/3cf121ed31f7a022e2ae6585391434d9c88e9792

errael commented 7 months ago

As long as you take care of the keys, there are no problems.

I'd like vim to provide a general solution. It gets pretty messy when things are imported from other files. And much more complex if you want to index by a class.

errael commented 7 months ago

Oops, made the comment to close (Basic issue taken...), but didn't close.

zzzyxwvut commented 7 months ago

If you really want eval(string(obj)) to work, eval would have to take some arbitrary string and decide if it looks like the output of some random string(obj) output and create an object from that; that seems unreasonable (if not impossible) and beyond what eval() does. Still looks like a new builtin, obj_from_string(string(obj)), makes more sense; and make sure you don't override string().

Let me, for the sake of argument, bounce around a trivial
alternative. Can't we help eval() beforehand by passing
it a string whose content it can parse? So, let's consider
a cooperation between string() (or any method whose return
type is string) and a defined for this task static factory
method whose return type is the type of obj (or its any
supertype) and whose sole parameter is any of eval()-
supported types, e.g. dictionary. In string(), we can store
necessary for future instantiation variables as elements of
a said dictionary and convert it to a string; in our factory
method, we can take that dictionary as the passed argument
and construct a new object from its values. In other words,
we resort to Foo.FromDict(eval(string({...}))) instead of
eval(string(obj)).

vim9script

class Pair
    var a: any
    var b: any

    def new(this.a, this.b)
    enddef

    def string(): string
    return string({a: this.a, b: this.b})
    enddef

    static def FromDict(dict: dict<any>): Pair  # Or any superclass.
    return Pair.new(get(dict, 'a', 0), get(dict, 'b', 0))
    enddef
endclass

const pair: Pair = Pair.new("alpha", "beta")
echo pair == Pair.FromDict(eval(string(pair)))

And we can have some luck with recursive types too:

vim9script

class Zero
    static const ZERO: Zero = Zero.new()

    def string(): string
    return string({})
    enddef
endclass

class Number extends Zero
    const next: Zero
    const value: number

    def new(this.next, this.value)
    enddef

    def string(): string
    return string({next: this.next, value: this.value})
    enddef

    static def FromDict(dict: dict<any>): Zero
    if empty(dict)
        return Zero.ZERO
    endif

    var numbers: list<number> = []
    insert(numbers, get(dict, 'value', 0))
    var next: any = get(dict, 'next', 0)

    while type(next) == type({}) && !empty(next)
        insert(numbers, get(next, 'value', 0))
        next = get(next, 'next', 0)
    endwhile

    var number: Number = Number.new(Zero.ZERO, 1)

    for value in slice(numbers, 1)
        number = Number.new(number, value)
    endfor

    return number
    enddef
endclass

var _0: Zero = Zero.ZERO
echo _0 == Number.FromDict(eval(string(_0)))

var _49: Number = Number.new(Zero.ZERO, 1)

for value in range(2, 49)   # E724 for 50 and greater (blob?).
    _49 = Number.new(_49, value)
endfor

echo _49 == Number.FromDict(eval(string(_49)))
errael commented 7 months ago

This example seems to be about serialization; and AFAICT, it requires knowing the type of the object in order to de-serialize. It's also a support problem, needing to maintain the to/from code when the class is changed. Seems like this manual technique requires retrofitting into any class that is part of the hierarchy; and superclass source may be owned by someone else. It doesn't seem trivial. It does seem that the output of default string() could be parsed to get a dict as string; there might be some kind of semi-automatic mechanism to be found.

Do you think there's a problem having/handling vim builtin to_obj/from_obj?

Seems there are several things to consider

Most importantly, at least to me, is using an object as a dict key. And I want to recover the original object, is not ==. So serialization isn't an option.

(I didn't look at the second example)

zzzyxwvut commented 7 months ago

Any well-implemented support that would automate translation
to/from is more than welcome. I have shared an approach fit
for our current state of support.

Let me clarify what I meant by ‘triviality’. Implementing
it would not be trivial for string() when not only this is
an object but its instance variables are also objects whose
variables may also be objects, etc. But it is ‘trivial’ in
the sense that you are spared from having to roll out your
own string parser -- piggyback on dictionaries and rely on
their checks for well-formedness for free.

Furthermore, judging by the output, values produced with
default string() implementations for dictionaries, objects,
and now enumerations look similar. They seem to contain all
data necessary to create a copy with eval(). There can be
a problem for hypothetical eval() support for class objects
about what non-default constructor to invoke (invariants may
be imposed to reject invalid arguments). Some cooperation
from class authors can be of help, e.g. if there is, say,
a conventionally named newDeserial() defined, use it; if
not, try a constructor having the most parameters.

As to is or ==, the support for is is out there, with
indirection. Consider using for the object keys the name(s)
of variables (possibly keyed or indexed) that hold these
objects. In the example below, I conveniently store objects
in another dictionary.

Copy adt.vim with a toy Set implementation (augmented).

File test.vim:

vim9script

# Generate A-Z Letter implementations and declare "g:abc_src".
source abc_gen.vim

execute 'import "' .. g:abc_src .. '" as abc'
import './adt.vim'

# Create a regular dictionary populated with object values to be used for keys
# elsewhere.  Give it a short-variable name for better key lookup.
const s: dict<abc.Letter> = abc.LETTERS
    ->reduce((d: dict<abc.Letter>, v: abc.Letter): dict<abc.Letter> =>
    extend({[v.AsString()]: v}, d),
    {})

# Create another dictionary, e.g. Set, and establish indirection by referring
# with each key to the object values of the first dictionary.
const set: adt.Set = adt.Set.newFromList(abc.LETTERS,
    (o: abc.Letter): string => 's.' .. o.AsString(),
    (t: string): abc.Letter => eval(t))

# Recover original objects from the dictionary keys.
for object in set.Elements()
    const name: string = object.AsString()
    echo s[name] (s[name] is object && object is abc.ABC.Get(name))
endfor

File abc_gen.vim:

vim9script

const head =<< END
vim9script

export interface Letter
    def AsString(): string
endinterface

enum None implements Letter
    NONE
    def AsString(): string
    return ""
    enddef
endenum

var abc: dict<Letter> = {}

enum Letters
    ABC
    def Get(letter: string): Letter
    return get(abc, letter, None.NONE)
    enddef
endenum

END

const src: string = tempname() .. '.vim'
writefile(head, src)

for A in map(range(65, 65 + 25), (_, v) => nr2char(v, 1))
    const a: string = tolower(A)
    const body =<< trim eval END
    class {A} implements Letter
    const {a}: string = '{a}'
    def new()
        abc.{a} = this
    enddef
    def AsString(): string
        return this.{a}
    enddef
    endclass

    abc.{a} = {A}.new()

    END

    writefile(body, src, 'a')
endfor

const tail =<< END
export const ABC: Letters = Letters.ABC
export const LETTERS: list<Letter> = sort(values(abc))
export const NONE: Letter = None.NONE
END

writefile(tail, src, 'a')
g:abc_src = src
errael commented 7 months ago

Any well-implemented support that would automate translation to/from is more than welcome.

I'm wondering if, as you've studied this issue, you've seen any issues that would create problems from doing to/from string with some vim support?

I've never done much with Java's serialization; I've seen that classes can intervene for some fixup and there's mechanisms for different versions of a class. I guess these issues are general and go beyond whether there's vim support.

It's not clear to me what use cases you have in mind.

zzzyxwvut commented 7 months ago

Do you think there's a problem having/handling vim builtin to_obj/from_obj?

Any well-implemented support that would automate translation to/from is more than welcome.

That is an encouragement to go for it when you really need
it. As I mentioned above, my Vim needs are usually within
its process. So if we had additional support from eval()
and {to,from}_obj, all the better.

When I want to save/restore, say, a Vim dictionary between
program instances, I would write it to a bespoke file in Vim
syntax and source the file on demand. Nothing fancy.