Value types are not cast to Cell<T> when passed to functions that take Object parameter #803

Closed
davidhesselbom opened this Issue Aug 19, 2014 · 14 comments

Projects

None yet

4 participants

@davidhesselbom
Contributor

Boxing and unboxing should could be transparent to the programmer so that any value type can be implicitly casted to an object and explicitly cast from an object.

The Cell class should be very well suited for this.

write: func(data: Object) {
    (match (data) {
        case value: Int => // in this case automatic unboxing should happen so that the below case is not relevant
            value toString()
        case cell: Cell<Int> =>
            cell get() toString()
        case =>
            "FAIL"
    }) println()
}
// This works
write(Cell new(2))
// This results in a segfault
write(2)

The last call should either not compile (if it is decided that value types cannot be passed as objects) or the 2 should be cast to, in this case, Cell<Int>.

This is called boxing and unboxing in e.g. C#: http://msdn.microsoft.com/en-us/library/yz2be5wk.aspx

@shamanas
Collaborator

This is an interesting idea.

We've always had problems with class vs cover in ooc, because of the fact that everything inherits from Object, even value types.

However, I don't think Object is intended to be used that way in ooc, when generics can do the job fine.

@simonmika

If it is not intended to be used this way then the last line in the example should create a compile error, not a runtime error. You are right that a generic function works in this case.

@shamanas
Collaborator

What I mean by this is that Object is not intended to be used for boxing/unboxing.

Perhaps the fact that covers inherit from Object is a mistake but the fact of the matter is that they are mostly designed and used to cover C types and many C libraries take an OO approach, which makes their types fit into the ooc typesystem really well, thanks to this.

Compound covers are quite unstable as of right now, I will try to figure something out to make them fit into the system more (perhaps passing a pointer implicitly when Object is expected, although this sounds like a bad idea) or at least implement plenty of warnings.

@davidhesselbom
Contributor

So, after reading this: http://fasterthanlime.com/blog/2015/ooc-generics-and-flawed-designs/ I tried

write: func <T> (data: T) {
    (match (data) {
        case value: Int =>
            value toString()
        case cell: Cell<Int> =>
            cell get() toString()
        case =>
            "FAIL"
    }) println()
}
// This works
write(Cell new(2))
// This also works
write(2) 

I'm pretty sure this solves my problem, as in, it lets me do what I want to do, I just didn't think of doing it that way. I'm still not sure I think the fact the first version builds, but crashes when run, isn't an error.

@fasterthanlime
Collaborator

@davidhesselbom you might want to have a suffixed function for the Int case, so that dispatch happens at compile time

@davidhesselbom
Contributor

Are you suggesting I avoid generic parameters altogether? I can have a suffixed function for every imaginable case, but then, what's the point of having generics? (I always found them to be confusing and strange, myself, heh.)

@fasterthanlime
Collaborator

@davidhesselbom If the type is known at compile-time then suffixes functions can be used for dispatch and it's more efficient. If it isn't know (e.g. you're dealing with a List or another generic collection) then you can use a generic function to do the dispatching at runtime yourself.

Of course if for example you have a class hierarchy like:

- Widget
|-- Button
|-- Label
|-- Window

And then Widget implements toString, then you wouldn't have a particular case for Button, Label, Window, just for the super-class Widget. I hope it's clearer that way :)

@fasterthanlime
Collaborator

I know it's confusing sometimes, but really, generics were introduced mainly for collections. If you think of them like that, then it makes a lot more sense.

@davidhesselbom
Contributor

True, it's more efficient. For some reason I thought you were saying I should avoid generic parameters, period :)

@fasterthanlime
Collaborator

Generics are just a means to an end, for ooc it's mostly collections, for other languages it might solve a lot more. It's okay. It's not a silver bullet :)

@simonmika

As long as the collections don't should contain value-types, or? (With the exception of when the length of the value type coincides with the length of a pointer.)---- On må, 19 jan 2015 06:12:15 -0800 Amos Wengernotifications@github.com wrote ----Generics are just a means to an end, for ooc it's mostly collections, for other languages it might solve a lot more. It's okay. It's not a silver bullet :)—Reply to this email directly or view it on GitHub.

@fasterthanlime
Collaborator

@simonmika I'm having trouble reading that sentence, but if you're wondering whether generics only works on types which size are exactly a pointer size, you're mistaken. Here's an example:

import structs/ArrayList

ABigStruct: cover {
    a, b, c, d: Double

    toString: func -> String {
        [a, b, c, d] as ArrayList<Double> map(|x| x toString()) join(", ")
    }
}

main: func {
    "ABS size         : #{ABigStruct size}" println()
    "ABS instanceSize : #{ABigStruct instanceSize}" println()

    list := ArrayList<ABigStruct> new()
    list add((1, 2, 3, 4) as ABigStruct)
    list add((2, 4, 6, 8) as ABigStruct)
    list add((9, 7, 5, 3) as ABigStruct)

    list each(|el|
        el toString() println()
    )
}

Output:

ABS size         : 32
ABS instanceSize : 32
1.00, 2.00, 3.00, 4.00
2.00, 4.00, 6.00, 8.00
9.00, 7.00, 5.00, 3.00

That's 32 bytes, so 256 bits. Much wider than a pointer anywhere that I know of :)

@fasterthanlime
Collaborator

For comparison's sake here's the same example with a class:

import structs/ArrayList

ABigClass: class {
    a, b, c, d: Double
    init: func (=a, =b, =c, =d) {}

    toString: func -> String {
        [a, b, c, d] as ArrayList<Double> map(|x| x toString()) join(", ")
    }
}

main: func {
    "ABC size         : #{ABigClass size}" println()
    "ABC instanceSize : #{ABigClass instanceSize}" println()

    list := ArrayList<ABigClass> new()
    list add(ABigClass new(1, 2, 3, 4))
    list add(ABigClass new(2, 4, 6, 8))
    list add(ABigClass new(9, 7, 5, 3))

    list each(|el|
        el toString() println()
    )
}

Output:

ABC size         : 8
ABC instanceSize : 40
1.00, 2.00, 3.00, 4.00
2.00, 4.00, 6.00, 8.00
9.00, 7.00, 5.00, 3.00

Because classes are by-reference, size here is the size of a pointer, ie. 8 bytes on the 64-bit machine from which I'm typing this message. Instance size is 32 (8 doubles) + 8 (pointer to the vtable).

Everything clear?

@fasterthanlime fasterthanlime added a commit that closed this issue Jul 10, 2015
@fasterthanlime fasterthanlime Closes #803 3c5055b
@fasterthanlime
Collaborator

I'm still not sure I think the fact the first version builds, but crashes when run, isn't an error.

Sure, it was a bug. Fixed now

@fasterthanlime fasterthanlime modified the milestone: 0.9.10 Jul 10, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment