In my code I have a proc that returns a cmp proc for sorting with algorithm.sorted:
type
Task* = object
isComplete*: string
priority*: string
# We could model the dates as `times.DateTime`, however the strings are
# fine for our purpose since we can sort them as strings due to the
# ISO 8601 format.
completionDate*: string
creationDate*: string
# Does include "+" prefixes
projects*: HashSet[string]
# Does include "@" prefixes
contexts*: HashSet[string]
# TODO: Add support for tags (key:value pairs)
# Original complete line
line*: string
# Everything after the "fixed-format" fields "x (A) 2020-01-05 2020-01-03"
description*: string
proc cmpTaskField*(fieldName: string): proc (task1, task2: Task): int =
case fieldName
of "isComplete":
return proc (task1, task2: Task): int =
cmp(task1.isComplete, task2.isComplete)
of "priority":
return proc (task1, task2: Task): int =
cmp(task1.priority, task2.priority)
of "completionDate":
return proc (task1, task2: Task): int =
cmp(task1.completionDate, task2.completionDate)
of "creationDate":
return proc (task1, task2: Task): int =
cmp(task1.creationDate, task2.creationDate)
of "line":
return proc (task1, task2: Task): int =
cmp(task1.line, task2.line)
of "description":
return proc (task1, task2: Task): int =
cmp(task1.description, task2.description)
cmpTaskField works (the last few statements are the relevant ones):
proc sortedTasks(tasks: seq[Task]; keysString = ""): seq[Task] =
## Sort `tasks` in-place according to the spec in `keysString`. For example,
## if `keysString` is "prio,crd", sort tasks first by priority, then by
## creation date.
if not keysStringIsAllowed(keysString, "x,prio,crd,cod,line,desc"):
raise newException(KeyError, &"invalid keys string \"{keysString}\"")
# According to the `algorithms` documentation, `sort` is stable, so we can
# sort first for the least significant key and last for the most significant
# key.
var tasks = tasks
let keyStrings = splitKeysString(keysString).reversed()
for keyString in keyStrings:
var
keyString = keyString
sortOrder = SortOrder.Ascending
if keyString.endsWith("-"):
sortOrder = SortOrder.Descending
keyString = strip(keyString, trailing = true, chars = {'-'})
let
fieldName = groupAndSortKeys[keyString]
cmpFunc = cmpTaskField(fieldName)
tasks = sorted(tasks, cmpFunc, order = sortOrder)
tasks
Now, obviously cmpTaskField has a lot of redundancy, so I'm trying to use a template:
template cmpTaskField*(fieldName: untyped): untyped =
proc cmp(task1, task2: Task): int = cmp(task1.fieldName, task2.fieldName)
var tasks = @[Task(isComplete: "b"), Task(isComplete: "a")]
tasks = sorted(tasks, cmp = cmpTaskField(isComplete))
#tasks = sorted(tasks, cmp = proc (task1, task2: Task): int = cmp(task1.isComplete, task2.isComplete))
echo tasks
(see also https://play.nim-lang.org/#ix=26CR for an - almost ;-) - runnable example)
However, I get the compiler message:
Hint: used config file '/playground/nim/config/nim.cfg' [Conf]
Hint: system [Processing]
Hint: widestrs [Processing]
Hint: io [Processing]
Hint: in [Processing]
Hint: algorithm [Processing]
/usercode/in.nim(25, 15) Error: type mismatch: got <seq[Task], cmp: void>
but expected one of:
proc sorted[T](a: openArray[T]; cmp: proc (x, y: T): int {.closure.};
order = SortOrder.Ascending): seq[T]
first type mismatch at position: 2
required type for cmp: proc (x: T, y: T): int{.closure.}
but expression 'cmp = proc cmp(task1`gensym135043, task2`gensym135044: Task): int =
result = cmp(task1`gensym135043.isComplete, task2`gensym135044.isComplete)
' is of type: void
proc sorted[T](a: openArray[T]; order = SortOrder.Ascending): seq[T]
first type mismatch at position: 2
unknown named parameter: cmp
expression: sorted(tasks, cmp = proc cmp(task1`gensym135043, task2`gensym135044: Task): int =
result = cmp(task1`gensym135043.isComplete, task2`gensym135044.isComplete)
)
It seems to me that somehow the proc "returned" from the template isn't visible at the place where it's used. As far as I understand the manual, the proc should be visible. (And adding {.inject.} to the proc doesn't change anything, so at least that's consistent. ;-)
What's missing? How should the code be changed and why?
(For the record, I read the section on templates in the Nim manual, but I understood at most half of it, so please be forgiving. :-) )
My understanding is that the template doesn't "return" a proc, rather it creates the code, at compile time, that the template says to generate. So by putting the template call inside your sorted call, you are actually creating a new proc definition in the middle of your call. If you call the template first, before the call to sorted, then it works, see: https://play.nim-lang.org/#ix=26D2
Does that still serve your purpose?
Re-reading your message, I think I didn't actually answer your question; you weren't asking "How can I make this work?" but more "Why does this approach fail?". Sorry!
At the moment I don't see why it isn't working either, but I experimented a bit more and got it to work using the => syntax for anonymous procs from the sugar module (https://nim-lang.org/docs/sugar.html): https://play.nim-lang.org/#ix=26Dx
/usercode/in.nim(25, 15) Error: type mismatch: got <seq[Task], cmp: void>
From this message is quite clear that your template didn't return the untyped as unchecked proc.
From your template definition, you just defined a proc that only available within that template scope and you didn't return anything. Simply add a line would imply you're returning something
template cmpTaskField*(fieldName: untyped): untyped =
proc cmpT(task1, task2: Task): int = cmp(task1.fieldName, task2.fieldName)
cmpT
Here you go: https://play.nim-lang.org/#ix=26DP, just one extra line and your code works.
The way this work is that templates are a way to insert premade AST into a specified location, which means:
template cmpTaskField*(fieldName: untyped): untyped =
proc cmp(task1, task2: Task): int = cmp(task1.fieldName, task2.fieldName)
tasks = sorted(tasks, cmp = cmpTaskField(isComplete))
generates:
tasks = sorted(tasks, cmp = proc cmp(task1, task2: Task): int = cmp(task1.fieldName, task2.fieldName))
and this is not valid Nim, because a named proc definition is a statement rather than an expression.
But the anonymous proc syntax proc () = does not work inside templates, so how can this be done? The answer is to define a proc then "return" its symbol:
template cmpTaskField*(fieldName: untyped): untyped =
proc cmp_custom(task1, task2: Task): int {.gensym.} = cmp(task1.fieldName, task2.fieldName)
cmp_custom # <--- that's the proc (task1, task2: Task): int that we need
The reason why I renamed cmp into cmp_custom is so that the compiler could know exactly which cmp are we targeting.
Thank you @leorize and @mashingan! It's basically like when you define a variable normally; it's a statement, not an expression so nothing is returned. Eg: in nim secret, if you type 5 you get 5 back, but if you type let x = 5, you get nothing back.
A question that I have is how do you make a template return a lambda (without using a macro like =>). Can you? I did some experimenting here: https://play.nim-lang.org/#ix=26E8 (turn on debug to see the macro treeRepr output).
Code reproduced below:
import algorithm, macros, sugar
type
Foo = object
x:int
var s = @[Foo(x:5),Foo(x:1),Foo(x:2)]
echo s
macro doit1(field:untyped):untyped =
result = newStmtList(quote do:
s.sorted(proc (f1, f2: Foo): int = cmp(f1.`field`,f2.`field`)))
echo "doit1"
echo result.treeRepr
macro doit2(field:untyped):untyped =
result = newStmtList(quote do:
proc myCmp(f1,f2:Foo):int = cmp(f1.`field`,f2.`field`)
s.sorted(myCmp))
echo "doit2"
echo result.treeRepr
macro doit3(field:untyped):untyped =
result = newStmtList(quote do:
let myCmp = proc (f1,f2:Foo):int = cmp(f1.`field`,f2.`field`)
s.sorted(myCmp))
echo "doit3"
echo result.treeRepr
macro doit4(field:untyped):untyped =
result = newStmtList(quote do:
s.sorted((f1,f2:Foo) => cmp(f1.`field`,f2.`field`)))
echo "doit4"
echo result.treeRepr
echo doit1(x)
echo doit2(x)
echo doit3(x)
echo doit4(x)
In doit1 and doit3, the comparator is a Lambda, and in doit2 it is a ProcDef (which is what I believe it was in @sschwarzer's template). In doit4 the comparator is a funny infix macro, which if you look at https://github.com/nim-lang/Nim/blob/version-1-0/lib/pure/sugar.nim#L105 you'll see is assigned a nodekind of Lambda as well. Can we mark the output of a template as a lambda (without going to macros)?Thanks a lot everyone. I'm glad it's so simple. :-) I thought I had tried this, but it seems I didn't. (On the other hand, defining the proc as an anonymous one actually was something I tried.)
Now I have a follow-up question. To replace my lengthy proc with the template, I have to create a cmp proc from the field name given as a string, but in the experiment in the Nim playground I used an identifier. Is there a relatively easy way to change the template so that it accepts the field name as a string? So instead of
tasks = sorted(tasks, cmp = cmpTaskField(isComplete))
I'd like
tasks = sorted(tasks, cmp = cmpTaskField("isComplete"))
I say "relatively easy" above because if this requires a macro that is much longer and complicated than my original proc, I'd rather use the proc in my code than the macro. :-)It's the other way around; I want to turn a string literal into an identifier.
The program parses a todo.txt file and displays the information grouped and sorted (grouping isn't implemented yet). The user can specify the sort criteria on the command line, for example,
$ todoreport --sort-by=prio,crd todo.txt
would print the tasks from the todo.txt file sorted by priority, then by creation date. So the user doesn't specify the field names for the Task object directly, but the user input is "translated" to the corresponding field name with a hash table.
What I'm looking for is something like Python's getattr. For example, getattr(someObject, "fieldName") would return the value of someObject.fieldName. Since Nim uses the term "field" instead of "attribute", a Nim version of this could be called getField.