No description
Find a file
haxscramper b72a9be176
[FIX]
2021-08-29 19:51:31 +03:00
src [FIX] 2021-08-29 19:51:31 +03:00
tests [FIX] 2021-08-29 19:51:31 +03:00
.gitignore [FIX] Missing handling of group reference for XSD 2021-06-05 20:58:41 +03:00
index.rst [FIX(algo)] 2020-08-28 21:12:13 +03:00
nimtraits.nimble v0.1.5 2021-05-12 21:45:57 +03:00
readme.org [FEATURE] Create Eq/Hash bodies standalone 2021-03-10 18:02:34 +03:00

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:

  • it value to be set to field
  • self current 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 - add impl_ 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 object
  • getApiName - 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 of T(kind: <kind>, field: <field1>). Similar to aggregate initalization from C++.