Hello fellow data scientist.

Your approach is not ideal but let's first correct your code.

This is the fixed one:
    
    
    type
      Element[T] = ref ElementObj[T]
      ElementObj*[T] = object
          parent: string
          data: T
    
    type
      Row*[T] = object
        columns: seq[string]
        elements: seq[Element[T]]
    
    proc newElement*[T](parent: string, data: T): Element[T] =
        new(result)
        result.parent = parent
        result.data = data
    
    var element = newElement("index", 12)
    echo element.parent
    echo element.data
    
    # The above compiles, when including the below it does not
    
    proc newRow*(T: typedesc): Row[T] =
      result.columns = @[]
      result.elements = @[]
    
    var row = newRow(int)
    
    # Output:
    #   - index
    #   - 12
    
    
    Run

Elements and Rows need the T marker for generics. Also if you have generic 
sequence say `seq[Element[T]]` you can hold either `seq[Element[int]]` or 
`seq[Element[float]]` but not both.

Why? Because at a low-level different types take different memory spaces. 
Python supports heterogeneous lists because each elements is hidden behind a 
pointer (32-bit size on 32 bit arch 64 bits on modern arch), we say that the 
types are `boxed`.

This is a form of `type erasure`

Here is an example:
    
    
    import typetraits
    
    type
      Element = ref object of RootObj
      ElementString = ref object of Element
        data: string
      ElementInt = ref object of Element
        data: int
    
    type
      Row*[T] = object
        columns: seq[string]
        elements: seq[Element]
    
    proc newElement*[T](parent: string, data: T): Element =
      when data is string:
        result = ElementString(data: data)
      elif data is int:
        result = ElementInt(data: data)
      else:
        {.fatal: "Unsupported type: " & T.name .}
    
    proc initRow*(): Row =
      result.columns = @[]
      result.elements = @[]
    
    method `$`(x: Element): string {.base.} =
      raise newException(ValueError, "Overload me!")
    
    method `$`(x: ElementString): string =
      x.data
    
    method `$`(x: ElementInt): string =
      $x.data
    
    let element = newElement("index", 12)
    let row = initRow()
    
    echo element
    echo row
    
    # Output:
    #   - 12
    #   - (columns: @[], elements: @[])
    
    
    Run

The other form of type erasure in Nim is through object variants, also called 
tagged unions:
    
    
    import typetraits
    
    type
      ElementKind = enum
        ekString, ekint
      Element = object
        case kind: ElementKind
        of ekString:
          sData: string
        of ekInt:
          iData: int
    
    type
      Row*
       = object
        columns: seq[string]
        elements: seq[Element]
    
    proc initElement*[T](parent: string, data: T): Element =
      when data is string:
        result = Element(kind: ekString, sData: data)
      elif data is int:
        result = Element(kind: ekInt, iData: data)
      else:
        {.fatal: "Unsupported type: " & T.name .}
    
    proc initRow*(): Row =
      result.columns = @[]
      result.elements = @[]
    
    let element = initElement("index", 12)
    let row = initRow()
    
    echo element
    echo row
    
    # Output:
    #   - (kind: ekint, iData: 12)
    #   - (columns: @[], elements: @[])
    
    
    Run

Now on the differences:

As you can see the first kind (Boxing) uses ref and inheritance, ref means that 
data is allocated on the heap. Allocation is **very** expensive when done in a 
loop. The advantage is that, if you write a library that uses inheritance your 
types can be extended by the users.

For the second kind, object variants, this is allocated on stack so much faster 
but cannot be user extended without forking a library. Also while you always 
have to use a case statement to check the "tag"/"kind", branch predictors 
nowadays are very good at that and this is much less costly than memory 
accesses. The main issue is that ergonomically wise the fields are not named 
the same but you can always write a proc data wrapper that selects the proper 
field.

Now in conclusion, the json module already did half the work for you, just 
reuse the [JsonNode type](https://nim-lang.org/docs/json.html#JsonNode). Also 
be sure to check [NimData](https://github.com/bluenote10/NimData).

Reply via email to