| src | ||
| tests | ||
| .gitignore | ||
| index.rst | ||
| nimtraits.nimble | ||
| readme.org | ||
readme
Proof-of-concept macro library for automatic rust-like trait
derivation. Provides API for user-defined traits, field renaming.
Currently implemented traits for equality (== operator), hash, field
validation (check value assertions for setting/getting values from
fields), immutable field annoations (automatically prevent
modification of the field).
Example
import hashes, tables, nimtraits
derive commonDerives:
type
Hhhhh {.derive(Hash, Eq).} = object
f1: float
f3: int
case f5: bool
of false:
f2: char
else:
f4: float
var hh: Table[Hhhhh, int]
hh[Hhhhh(f3: 12)] = 1231
echo hh
{(f1: 0.0, f3: 12, f5: false, f2: '\x00'): 1231}
Regular use API
Eq trait
Zero configuration necessary - add {.derive(Eq).} to the object
annotation and == proc will be automatically declared.
expandMacros:
derive commonDerives:
type
Test {.derive(Eq).} = object
f2: float
case f3: bool
of true:
f4: int
else:
f5: string
type
Test = object
f2: float
case f3: bool
of true:
f4: int
else:
f5: string
proc `==`(lhs: Test; rhs: Test): bool {.noSideEffect.} =
if (
not (lhs.f2 == rhs.f2)):
return false
if (
not (lhs.f3 == rhs.f3)):
return false
if lhs.f3 == rhs.f3:
case lhs.f3
of true:
if (
not (lhs.f4 == rhs.f4)):
return false
else:
if (
not (lhs.f5 == rhs.f5)):
return false
return true
Hash trait
Declare hash function for object.
Validate trait
Annotate fields with {.check(<bool expression>).} that will run each
time you set a value to a field. If assertion fails ~ValidationError
is raised.
import strutils
derive commonDerives:
type
Type {.derive(Validate).} = object
fld {.check(it.startsWith("hhh")).}: string
var t: Type
try:
t.fld = "####"
except ValidationError:
echo getCurrentExceptionMsg()
Error while validating field 'Type.fld': assertion 'it.startsWith("hhh")' failed.
Two constants are accessible in the check expression:
itvalue to be set to fieldselfcurrent object
Implementing custom traits
Macro derive accepts static parameter: DeriveConf which contain
list all callbacks for building proc declarations. Each callback is a
function with signature func(var Object, DeriveParams): NimNode.
First argument is a current representation of the object using type
from hmisc/types/hnim_ast and second one is a set of common
configuration parameters.
NOTE: ! object description is passed as mutable parameter - it means that particular trait implementation is allowed to change object structure. It is not possible to completely control changes, but for field renaming API is provided.
renameInternal- addimpl_prefix to object, making field internal. Subsequent renames do nothing, which means it can be used as many times as necessary, by completely unrelated traits.getInternalName- get actual name of the field in the objectgetApiName- get name of the field as visible for 'external users'.
import nimtraits, macros
import hmisc/types/hnim_ast
func makeEchoImpl(obj: var Object, params: DeriveParams): NimNode =
let
self = ident "self" # name of the object for case statement.
impl = self.eachCase(obj) do(fld: TraitField) -> NimNode:
# Iterate each field in object and execute callbac for it.
let
fldName = newLit fld.getApiName()
# To avoid collision betteen different traits that might
# modify field names it is necessary to use `getApiName` -
# to get public field name
fldId = ident fld.getInternalName()
# `getInternalName` returns actual name of the field in
# object
quote do:
echo "Field '", `fldName`, "' has value ", `self`.`fldId`
# Result of trait implementation callback can be anyting - in
# this case it is a procedure declaration. Helper proc builder
# from `hmisc/types/hnim_ast` is used.
result = (ident "echoAll").mkProcDeclNode(
{ "self" : obj.name },
impl,
exported = false
)
debugecho $!result
const deriveConf = DeriveConf( # Create custom set of trait
# configurations
traits: @[
TraitConf(
name: "EchoFields", # <-----------------+
implCb: makeEchoImpl)]) # |
# |
derive deriveConf: # Use it in derive macro |________
type #__________ annotate type with `EchoFields`
A {.derive(EchoFields).} = object
case bbb: bool
of true:
zzz: int
else:
qqq: char
let test = A(bbb: false, qqq: 'e')
test.echoAll()
proc echoAll(self: A) =
echo "Field \'", "bbb", "\' has value ", self.bbb
case self.bbb
of true:
echo "Field \'", "zzz", "\' has value ", self.zzz
else:
echo "Field \'", "qqq", "\' has value ", self.qqq
Field 'bbb' has value false
Field 'qqq' has value e
Contribution & development
This is a proof-of-concept library - any input on API design, questions and suggestions about implementation are more than welcome. My discord server.
TODO
- automatically add object documentation with explanation for all changes made to object. require trait implementation callbacks to document their actions. Might use some kind of simple template (based on strtabs & simple string interpolation) to make documentation. It will be a little verbose, yes, but better than looking at object in documentation and figuring out all transformations.
- Add original object declaration in the documentation too? It will retain all semantic information about derive annotations: e.g. you can actually see the original intention.
- Generate both mutable and immutable getters to allow using
fields like
obj.fld[2] = "value" - Asigning from varargs and haskell-like object constructors. Create
discrimintant objects using
initT <kind>(<args> ...)instead ofT(kind: <kind>, field: <field1>). Similar to aggregate initalization from C++.