Home > Mobile >  How to map seq[type] to seq[type.field] with a generic fieldname?
How to map seq[type] to seq[type.field] with a generic fieldname?

Time:01-14

I am building a web application and using an ORM called norm. As such, I have an SQL database with a bunch of tables, all of which correspond to various Models that I have in nim. Often enough, they are in many-to-many relationships such as this:

type 
  A = ref object of Model
    name: string
  B = ref object of Model
    anotherName: string
  C = ref object of Model
    myA: A
    myB: B

Due to the way norm is currently implemented, I have to query C if I want all entries B for a given A or all entries A for a given B.

That leaves me with a seq[C] when I actually wanted seq[A] or seq[B].

I can, of course, for a specific set of A, B and C iterate over the seq with the mapIt function of sequtils:

import sequtils
let myASeq: seq[A] = myCSeq.mapit(it.myA)

But given that I have around 20 or so of these relationships and I'd like to not implement the same thing 20 times there. I'd prefer a generic way, where I can just type in the fieldName (e.g. myA) in some way and resolve it like that on the fly. How can I achieve this?

CodePudding user response:

Thanks to the incredibly helpful folks on the nim discord server (shoutout to ElegantBeef), that educated me on this. There is 2 ways you can go about this:

1. If you know the field name when writing the code, you can use a template

This is preferred, as templates are simpler to understand than macros.
import norm/[model, pragmas]
import std/[sequtils, typetraits, macros, json] 

template mapModel[T: Model](mySeq: seq[T], field: untyped): seq[untyped] = mySeq.mapIt(it.field)

Example usage:

type
    A = ref object of Model
        name: string
    D = ref object of Model
        myothernameid: string
        myDA: A

var myDSeq: seq[D] = @[]
let anA: A = A(name: "this is an A")
myDSeq.add(D(myothernameid: "la", myDA: anA))
myDSeq.add(D(myothernameid: "le", myDA: anA))

echo %*myDSeq # [{"myothernameid":"la","myDA":{"name":"this is an A","id":0},"id":0},{"myothernameid":"le","myDA":{"name":"this is an A","id":0},"id":0}]

let myASeq: seq[A] = mapModel(myDSeq, "myDA")

echo %*myASeq # [{"name":"this is an A","id":0},{"name":"this is an A","id":0}]

2. If you only know the name of specific field at compile time as a static string, you can use a macro

This one is useful if you only have access to the field name at compile time and not the field itself, because that might be different depending on a when condition.

import norm/[model, pragmas]
import std/[sequtils, typetraits, macros, json] 

macro mapModel[T: Model](mySeq: seq[T], field: static string): untyped =
    newCall(bindSym"mapIt", mySeq, nnkDotExpr.newTree(ident"it", ident field))

Example usage:

var myDSeq: seq[D] = @[]
let anA: A = A(name: "this is an A")
myDSeq.add(D(myothernameid: "la", myDA: anA))
myDSeq.add(D(myothernameid: "le", myDA: anA))

echo %*myDSeq # As Before

let myASeq: seq[A] = mapModel(myDSeq, "myDA")

echo %*myASeq # As Before

For an explanation what the macro does, I do not have a full understanding of it myself, but wrote down what I got out of my chat with ElegantBeef:

newCall --> You're about to receive something to execute which I'll refer to as callable, do that

bindSym"mapIt" --> Call a function that is bound to the symbol "mapIt", and do so with the following arguments

mySeq --> the first argument to pass to the function of the "mapIt" symbol

nnkDotExpr.newTree --> This is going to be an execution tree or whatever they're called this resolves into a callable, such as a proc or a func. Specifically a callable that is a dot expression.

ident"it" --> The value of the variable that has the symbol "it", this is supplied by the "mapIt" function

ident field --> The value of the variable that has the symbol contained within the field variable.

nnkDotExpr.newTree(ident"it", ident field) --> Call a dot expression on it to grab the value of its field that goes by the name contained within field. This is equivalent to it.la if field contains "la"

  •  Tags:  
  • Related