#Am I doing polymorphism right, and why do I get a crash?

1 messages · Page 1 of 1 (latest)

muted lion
#

I'm not sure this can be called polymorphism, but whatever. I'm trying to have an "object" that can be of several types with divergent properties, and so I'm using unions, as they seem the right tool for the job.

But I'm used to OOP, so my understanding of this paradigm is still weak, so I'd like it if someone could look at my code and tell me if it looks ok, and what could be improved.

I'm also getting a crash when I raise the values of W and H too much, and I can't figure out why. I was having this exact problem in my real project and I reproduced it here.

Here's my full test code:


package polym

import "core:fmt"
print := fmt.println

W, H :: 2, 2 // 320, 200    // crashes when using high values

Foo :: struct {
    name:string, 
    f:f32, 
    l:[2][W][H]int
}
Bar :: struct {
    name:string, 
    i:int, 
    l:[2][W][H]int
}
Thing :: union {Foo, Bar}

init_foo :: proc() -> Foo { 
    return Foo {name="foo", f=1} 
}
init_bar :: proc() -> Bar { 
    return Bar {name="bar", i=2} 
}
init_thing :: proc(which:string) -> Thing {
    switch which {
        case "foo": return init_foo()
        case:       return init_bar()
    }
}

use_foo :: proc(t:Thing) { 
    print(t.(Foo).name) 
}
use_bar :: proc(t:Thing) { 
    print(t.(Bar).name) 
}
use_thing :: proc(t:Thing) {
    switch v in t {
        case Foo: use_foo(t)
        case Bar: use_bar(t)
    }
}

main :: proc() {
    foo := init_thing("foo")
    bar := init_thing("bar")

    use_thing(foo)
    use_thing(bar)
}
lunar kayak
#

You probably want to make use_foo/use_bar take the explicit type and not the union

muted lion
#

~~ crap, I just screwed the OP..., trying to improve the code... give me a few minutes...~~
Done... pheew...

lunar kayak
#

The crash you might be seeing is that you're using fixed arrays for l

#

They would be allocated on the stack

#

Say you used 320 and 200

#

That would be 2 * 320 * 200 * size_of(int)

#

Which is just over 1 million bytes

#

Since you've pushed 2 of them to the stack that's roughly 2 megabytes

#

On windows the default stack size is 1 megabyte so you stack overflow and crash

muted lion
#

oh!

swift creek
#

The simplest way to fix this quickly would be to just have the init procs return a pointer, and then use new(Foo) and new(Bar) respectively.

muted lion
#

I did have the compiler warning me about that a while ago

swift creek
#

In fact, you can do:

return new_clone(Foo {name="foo", f=1})
#

new_clone does a new followed by assigning the given value to the new memory, and then returns it.

muted lion
#

the init_thing is completely redundant anyway...

#

@swift creek something like this?

new_foo :: proc() -> ^Foo { return new_clone(Foo {name="foo", f=1}) }
muted lion
#

well this seems to work, but I'm not sure if type assertions are the way to go
(if I don't dereference the pointers for use_thing then it breaks in ways I can't figure out)

new_foo :: proc() -> ^Foo { return new_clone(Foo {name="foo", f=1}) }
new_bar :: proc() -> ^Bar { return new_clone(Bar {name="bar", i=2}) }

use_foo :: proc(t:Foo) { print(t.name) }
use_bar :: proc(t:Bar) { print(t.name) }
use_thing :: proc(t:Thing) {
    switch v in t {
        case Foo: use_foo(t.(Foo))
        case Bar: use_bar(t.(Bar))
    }
}

main :: proc() {
    foo := new_foo()
    bar := new_bar()

    use_thing(foo^)
    use_thing(bar^)
}
swift creek
#

The actual use to having a creation proc is that if you don't allocate it, you might crash.
So it helps in that way.

swift creek
muted lion
#

oh! it was right there and I was completely oblivious to it...

swift creek
#

t is a thing, but use_foo/use_bar both do not take Things - they take the specific values - hence why that wouldn't work for you.

muted lion
#

should I be dereferencing the pointers though?

#

not sure what's more convenient

#

although, again, I'm not quite sure how I would have to change use_thing to take in a ^Thing and pass along the pointers to the correct types to the correct procedures

#

in fact, it seems I can't even just pass ^Foo to use_thing(t:^Thing) in the first place

.../polymorphism.odin(28:15) Cannot assign value 'foo' of type '^Foo' to '^Thing' in argume
nt
.../polymorphism.odin(29:15) Cannot assign value 'bar' of type '^Bar' to '^Thing' in argume
nt
swift creek
#

You'd either leave them as they are and deref, or you'd pass pointers. Up to you whether or not the procs need to mutate

muted lion
#

kinda seems I can't pass pointers

swift creek
muted lion
#

but also when I do use_thing(foo) it gives me the errors above, because use_thing takes a ^Thing, not a ^Foo

swift creek
#

Only indeed, that will not work.

#

The two things have entirely different memory layouts, as Things have a tag, and a Foo/Bar doesn't.

#

If you want a Thing to pass through, then I'd just make new_foo etc return Things or ^Things instead.

muted lion
#

I'm doing a game of life as an exercise, but there are multiple GoL algorithms I like to experiment with, and I'm trying to do this in a way that I can have a single interface for all of them, so that before compiling, I just set choose which algorithm to use, and I don't have to change anything else.

#

I don't know if there's better ways to do this

#

I thought of creating a struct to represent the data that each algorithm needs, but they have some differences

LifeSimple :: struct {        // simple naive algorithm
    ALIVE              : int,
    wrap_around        : bool,
    curr               : int,
    prev               : int,
    cells              : [2][][]int,
}

LifeAbrash :: struct {        // Michael Abrash's algorithm
    ALIVE              : int,
    COUNT              : int,
    wrap_around        : bool,
    cells              : [2][][]int,
    board              : int,
    buff               : int,
}
lunar kayak
#

You can merge them and just use the ones you need for the algorithm

muted lion
#

many functions also work differently internally

#

in fact, the board and buffer for LifeAbrash might be better of as slices of bit sets, and I don't need them in an [2]array, I can have them separated (the [2]array allows for easily swapping boards in the Simple algorithm, but this is irrelevant for Abrash's)

#

so, if I merged them I think I might have a lot of unused stuff in memory, especially because there are even more algorithms I would like to add

lunar kayak
#

How much memory do you think that will be?

muted lion
#

I don't know, that will depend on the size of the grid, and on each algorithm's requirements

lunar kayak
#

But your grid size is using slices

#

So they're allocated at runtime

#

So you only allocate what you need for the algorithm

muted lion
#

yes, I switched to slices now so I don't have that stack problem

#

but I still don't like the idea of merging them...

#

at some point the struct would be a mess

#

I don't know if all the next algorithms will be possible to adapt to this, but I'd like to try

#

I'm doing this as an exercise to learn more about Odin, anyway

swift creek
# muted lion at some point the struct would be a mess

FYI, doing the so-called 'megatruct' approach is generally quite handy when you're not sure how to architect something.
Because its easy to do, ultra flexible to your logic requirements, and low friction to work with.

#

It is basically just a tagged union, but without the data being actually union'd.

muted lion
#

how would I then redirect it to the respective procs?

#

I suppose I could do it with a name string or an enum

#

I still don't like it, though...

#

I'm having another problem with the current setup, I can't reassign the fields

use_foo :: proc(t:Foo) {
    t.name = "fooings"     // error: Cannot assign to 't.name'
    print(t.name)
}
swift creek
#

If you want to mutate, pass a pointer.

swift creek
#

The reason that approach is so flexible is specifically because everything is the same, and everything has all the data.

muted lion
#

the thing is use_foo and use_bar don't work the same

#

but ok, I can see ways to redirect from use_thing

muted lion
swift creek
#

Not ^Thing

#

Then you can switch &v in thing { case Foo: use_foo(&v) }

#

FWIW though, having used unions for something myself, I generally find it easier to just have everything deal with the union itself, rather than the variants, as I found myself wanting access to it more often than I didn't, and/or saving myself from bugs about where the variant value was, etc.

muted lion
#

in order for new_foo to return ^Thing, doesn't that require using the megatruct instead?

#

becasue this doesn't work

new_foo :: proc() -> ^Thing { return new_clone(Foo {name="foo", f=1}) }
#

probably for obvious reasons that weren't obvious to me until I tried it

muted lion
#

as in, passing in Thing to every function, is this case?

swift creek
#

Remember, a Thing is a struct { payload: [N]byte, tag: u8, } essentially.
But a Foo is just its own data, and nothing more - there's no tag.
This means that you cannot reasonably convert a ^Foo to a ^Thing.

muted lion
#

@swift creek

Remember, a Thing is a struct { payload: [N]byte, tag: u8, } essentially.

is that the internals of a union?

swift creek
muted lion
#

not sure what's missing here now

use_thing :: proc(t:^Thing) {
    switch &v in t {  //    <----   error: Expected an identifier, got 'identifier'
        case Foo: use_foo(v)
        case Bar: use_bar(v)
    }
}
#

the goal there is to pass along pointers to Foo and Bar, but I don't think I'm on the right track

swift creek
#

If switch &v in t doesn't work, try switch v in &t.
Latest Odin changed this.

muted lion
#

I'm in an older version of the compiler because I'm still stuck with windows 7 for now