0

Would it be possible to write list.findBy(key, value) macro, so that:

let people = @[(name: "John"), (name: "Sarah")]
echo people.findBy("name", "John")

Ideally it should validate "name" at compile time.

I tried some code, but it doesn't work, play:

import macros, options

macro findBy*[T](list: seq[T], field: string, value: untyped): Option[T] =
  quote do:
    for v in list:
      if v.`field` == value: return

let people = @[(name: "John"), (name: "Sarah")]
echo people.findBy("name", "John")
Alex Craft
  • 13,598
  • 11
  • 69
  • 133

2 Answers2

3

You can use @Jason's implementation, though in this partuclar case tempate would be sufficient (not necessary to use quote do)

import std/[macros, options]

template findBy*[T](list: seq[T], field: untyped, value: untyped): Option[T] =
  var res: Option[T] # Create result variable
  for v in `list`: # Splice all parameters into loop
    if v.`field` == `value`: 
      res = some(v)
    
  res # 'return' from template is a final expression

let people = @[(name: "John"), (name: "Sarah")]

expandMacros:
  echo people.findBy(name, "John")
                   # ^ Note that `name` is passed as identifier, not string

This would print

Some((name: "John"))

And expanded code for findBy would look like this:


echo [
  var res`gensym0: Option[T]
  for v`gensym0 in items(people):
    if v`gensym0.name == "John":
      res`gensym0 = some(v`gensym0)
  res`gensym0]
haxscramper
  • 775
  • 7
  • 17
2

Here is a functioning implementation.

import macros, options, typetraits

macro findBy*[T](list: seq[T], field: untyped, value: untyped): untyped =
  quote do:
    var res = none(`list`.elementType)
    for v in `list`:
      if v.`field` == `value`:
        res = some(v)
        break
    res

let people = @[(name: "John"), (name: "Sarah")]
echo people.findBy(name, "John")


Jason
  • 385
  • 1
  • 4