Module classy

Classy

Provides ability to define and instantiate haskell-like typeclasses in Nim.

Overview

This module consists of two macros. typeclass saves a parameterized AST to serve as a template (in an object of type Typeclass). instance performs necessary substitutions in saved AST for provided arguments, and executes the resulting code.

As an example, let's say we want to define a Functor typeclass:

typeclass Functor, F[_]:
  # proc fmap[A, B](fa: F[A], g: A -> B): F[B]
  proc `$>`[A, B](fa: F[A], b: B): F[B]=
    fmap(fa, (a: A) => b)

This code does not declare any procs - only a compile-time variable Functor. Notice that our code abstracts over type constructor - F is just a placeholder.

Notice that we fmap is not part of the typeclass. At the moment forward declarations don't work for generic procs in Nim. As we'll see, even if proc AST in our typeclass has no generic parameters, the generated proc can have some. So it is recommended to not define unimplemented procs inside typeclasses. This will probably change after this issue is closed: https://github.com/nim-lang/Nim/issues/4104.

Now let's instantiate our typeclass. For example, let's define a Functor instance for all tuples of two elements, where the left value is SomeInteger:

instance Functor, (C: SomeInteger) => (C, _)

This generates the following proc definition:

proc `$>`[A, B, C: SomeInteger](fa: (C, A), b: B): (C, B)=
    fmap(fa, (a: A) => b)

Here are the few things to notice:

  1. All ocurrences of form F[X] were replaced with (C, X)
  2. C, the parameter of our instance, became the parameter of the generated definition, with its constraint preserved.
  3. We're referring to a proc fmap. So any procs, that are assumed to be present in typeclass body, should be defined with corresponding types before the typeclass is instantiated

Types

Typeclass = object
  name: string
  patterns: seq[AbstractPattern]
  body: NimNode
  Source Edit

Macros

macro typeclass(id, patternsTree: untyped; args: varargs[untyped]): typed

Define typeclass with name id.

This creates a compile-time variable with name id.

Call syntax:

typeclass Class, [A[_, ..], B,...], exported:
  <typeclass body>

Typeclass can have zero or more parameters, each of which can be a type constructor with arity 1 and higher (like A in sample above), or be a concrete type (like B). If a typeclass has exactly one parameter, the brackets around parameter list can be omitted.

The exported option allows to export the typeclass from module. This marks corresponding variable with export postfix. Notice that in this case standard restrictions apply: the typeclass call should be in module's top scope.

  Source Edit
macro instance[](class: static[Typeclass]; argsTree: untyped;
                options: varargs[untyped]): untyped

Instantiate typeclass class with given arguments

Call syntax:

instance Class, (K, L) => [AType[_, K,..], BType,...],
  skipping(foo, bar),
  exporting(_)

instance does the following:

  1. Replaces each of parameter forms in typeclass definition with corresponding argument form (like AType in code example). Parameter form list of typeclass and argument form lists of corresponding instance calls must have matching length, and corresponding forms should have the same arity.
  2. Injects instance parameters (K in the example) into top-level routines in typeclass body.
  3. Transforms the body according to options.
  4. Executes the body.

Instance parameters can have constraints in the same form as in a generic definition. If no instance parameters are present, the corresponding list can be omitted. If exactly one instance parameter is present (without constraints), the parenthesis around parameter list can be omitted.

Instance argument forms are trees with zero or more nodes replaced with wildcards (_), the number of wildcards in a tree corresponding to the form arity. A form can include existing types, as well as instance parameters.

Here are few valid parameter-argument combinations:

[]
int
Option[_]
[int, string]
K => Option[K]
(K: SomeInteger, L) => [(K, L, Option[_]), (L, int)]

Supported instance options:

  1. skipping(foo, bar...) - these definitions will be skipped.
  2. exporting(foo, bar) - these generated definitions will have export marker.
  3. exporting(_) - all generated definitions will have export marker.
  Source Edit