Search code examples
smalltalkpharo

How do I store the type of an object in a variable?


I have a class that needs to be passed the type of an element, so I can later check if another object has either the type or is a subclass of this type and add it to an internal collection.

I have an initialize: method that gets called from the new: ctor of my class:

initialize: aType
    elements := OrderedCollection new.
    type := aType class.

Now I have a method that gets passed a value and should check if the types are compatible:

add: anElement  
    type isNil ifTrue: [ elements add:anElement. ^self. ].

    (anElement isMemberOf: type)
       ifTrue: [elements add:anElement.]
       ifFalse: [ ^ 'Not supported!' ].

This works if I want to check for a concrete type:

|myClass|
myClass:= MyClass new: '123'.

cc add: '5.4'. "Works"
cc add: 123.  "Fails correctly."

Now, to check if it's a derived type I modified the add:method:

add: anElement  
    type isNil ifTrue: [ elements add:anElement. ^self. ].

    (anElement isKindOf: type)
      ifTrue: [elements add:anElement.]
      ifFalse: [ ^ 'Not supported!' ].

However, this doesn't work:

|myClass|
myClass:= MyClass new: 5 asNumber.

myClass add: 5.4. "Fails, although Float is a sub type of Number"

I suspect that my initial method of determining the type of an object (aType class) is wrong, but I can't find a better, or more explicit way of determining the type. Basically, I'm looking for something like typeOf(MyObject) in C#. This is part of an exercise, so please excuse the contrived example :)


Solution

  • As I mentioned in a comment to your question, the problem is that 5 asNumber is 5 which is an instance of SmallInteger, not an instance of Number. Thus, when you initialize: your class with 5 what you get in the ivar type is SmallInteger. And then, when you add: 5.4, the check becomes 5.4 isKindOf: SmallInteger, which naturally fails.

    I think that the problem originates in the way you have chosen to initialize the instance. A simpler approach would be to explicitly set the target type with a class, not an instance. Something on the lines of

    initialize: aClass
      elements := OrderedCollection new.
      type := aClass
    

    Then, your example will be something like

    |myClass|
    myClass:= MyClass new initialize: Number.
    myClass add: 5.4.
    

    which would accept 5.4 as an element because it is a Float, which isKindOf: Number.

    Now let me add another remark. The usual semantics of new: is different from the one you used. The argument of new: is usually an Integer and such and integer expresses the desired size of the new instance. For example, you say Array new: 3 when you want an Array with 3 entries, etc. It is not expected for new: to receive other kind of parameter for the construction of the object. I'm not saying it's forbidden, just that is not the usual naming convention. In your case I would suggest a method for instance creation such as

    MyClass class >> on: aClass
      ^self new initialize: aClass
    

    and your code would look like

    | sequence |
    sequence := MyClass on: Number.
    sequence add: 5.                        "ok, 5 isKindOf: Number"
    sequence add: 4.5.                      "ok, 5.4 isKindOf: Number"
    sequence add: 'hello world'             "fail, not a Number"