├── .gitignore ├── classy.nimble ├── config.nims ├── .travis.yml ├── license.md ├── readme.md ├── example.nim ├── tests └── test_classy.nim └── classy.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | /bin/ -------------------------------------------------------------------------------- /classy.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.0.5" 4 | author = "nigredo-tori" 5 | description = "typeclasses for Nim" 6 | license = "Unlicense" 7 | 8 | # Dependencies 9 | 10 | requires "nim >= 0.15.2" 11 | 12 | # Layout 13 | 14 | installFiles = @["classy.nim"] 15 | binDir = "bin" 16 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | import ./classy.nimble 2 | import ospaths 3 | 4 | # Tasks 5 | 6 | proc createDirs() = 7 | mkDir binDir 8 | 9 | task tests, "Run all tests": 10 | exec "nim test_classy" 11 | exec "nim example" 12 | 13 | task test_classy, "Run classy API test": 14 | createDirs() 15 | switch("out", binDir / "test_classy".toExe) 16 | --run 17 | setCommand "c", "tests/test_classy.nim" 18 | 19 | task example, "Run example": 20 | createDirs() 21 | switch("out", binDir / "example".toExe) 22 | --run 23 | setCommand "c", "example.nim" 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | env: 3 | # Nim versions to test against 4 | - CHOOSENIM_CHOOSE_VERSION=devel 5 | - CHOOSENIM_CHOOSE_VERSION=1.4.4 6 | - CHOOSENIM_CHOOSE_VERSION=1.2.6 7 | - CHOOSENIM_CHOOSE_VERSION=1.0.6 8 | - CHOOSENIM_CHOOSE_VERSION=0.20.2 9 | - CHOOSENIM_CHOOSE_VERSION=0.19.6 10 | - CHOOSENIM_CHOOSE_VERSION=0.18.0 11 | - CHOOSENIM_CHOOSE_VERSION=0.17.2 12 | - CHOOSENIM_CHOOSE_VERSION=0.16.0 13 | - CHOOSENIM_CHOOSE_VERSION=0.15.2 14 | 15 | install: 16 | - curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y 17 | before_script: 18 | - set -e 19 | - export CHOOSENIM_NO_ANALYTICS=1 20 | - export PATH=~/.nimble/bin:$PATH 21 | script: 22 | - nim --cc:$CC tests 23 | branches: 24 | except: 25 | - gh-pages 26 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Unlicense (Public Domain) 2 | ============================ 3 | 4 | This is free and unencumbered software released into the public domain. 5 | 6 | Anyone is free to copy, modify, publish, use, compile, sell, or 7 | distribute this software, either in source code form or as a compiled 8 | binary, for any purpose, commercial or non-commercial, and by any 9 | means. 10 | 11 | In jurisdictions that recognize copyright laws, the author or authors 12 | of this software dedicate any and all copyright interest in the 13 | software to the public domain. We make this dedication for the benefit 14 | of the public at large and to the detriment of our heirs and 15 | successors. We intend this dedication to be an overt act of 16 | relinquishment in perpetuity of all present and future rights to this 17 | software under copyright law. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 23 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 24 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | For more information, please refer to <> 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Classy 2 | 3 | [![Software License][ico-license]](license.md) 4 | ![Stability][ico-stability] 5 | [![Travis][ico-travis]](https://travis-ci.com/nigredo-tori/classy) 6 | 7 | [![nimble](https://raw.githubusercontent.com/yglukhov/nimble-tag/master/nimble_js.png)](https://github.com/yglukhov/nimble-tag) 8 | 9 | Haskell-style typeclasses for Nim. 10 | 11 | Allows to instantiate collections of functions for a given type or type constructor. 12 | 13 | ## Install 14 | 15 | ```bash 16 | $ nimble install classy 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```nim 22 | import classy, future 23 | 24 | typeclass Functor, F[_]: 25 | # proc map[A, B](fa: F[A], g: A -> B): F[B] 26 | proc `$>`[A, B](fa: F[A], b: B): F[B] = 27 | fa.map((a: A) => g) 28 | 29 | instance Functor, seq[_] 30 | assert: (@[1, 2, 3] $> "a") == @["a", "a", "a"] 31 | ``` 32 | ## Documentation 33 | 34 | Module documentation is located [here](https://nigredo-tori.github.io/classy/head/classy.html). 35 | 36 | Also refer to [example](example.nim) for a quick tutorial. 37 | 38 | ## Testing 39 | 40 | ```bash 41 | $ nimble tests 42 | ``` 43 | 44 | ## Stability 45 | 46 | Highly experimental. API and behaviour subject to change. 47 | 48 | ## Credits 49 | 50 | - [nigredo-tori][link-author] 51 | 52 | ## License 53 | 54 | The Unlicense. Please see [License File](license.md) for more information. 55 | 56 | [ico-license]: https://img.shields.io/badge/license-Unlicense-brightgreen.svg?style=flat-square 57 | [ico-stability]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square 58 | [ico-travis]: https://img.shields.io/travis/nigredo-tori/classy/master.svg?style=flat-square 59 | 60 | [link-author]: https://github.com/nigredo-tori 61 | -------------------------------------------------------------------------------- /example.nim: -------------------------------------------------------------------------------- 1 | import classy, options, future 2 | 3 | # We have to define a typeclass before we can create instances. 4 | # Parts of declaration: 5 | # - `Monoid` - name of the typeclass being defined 6 | # - `M` - placeholder for typeclass member 7 | # - `exported` declaration makes typeclass accessible from another modules 8 | # 9 | # Notice that `mempty` and `mappend` are not defined. These should be 10 | # implemented by user before instantiating typeclass. 11 | typeclass Monoid, M, exported: 12 | # proc mempty(t: typedesc[M]): M 13 | # proc mappend(a, b: M): M 14 | 15 | # Marker proc to be used later 16 | proc isMonoid(_: typedesc[M]) = discard 17 | proc mconcat(ms: varargs[M]): M = 18 | result = mempty(M) 19 | for m in ms: 20 | result = mappend(result, m) 21 | 22 | # We can now define `Monoid` instances for any types we want 23 | proc mempty(t = string): string = "" 24 | proc mappend(a, b: string): string = a & b 25 | 26 | # For `string` we can write optimized `mconcat` implementation. 27 | proc mconcat(ss: varargs[string]): string = 28 | result = "" 29 | for s in ss: result.add(s) 30 | 31 | # - `skipping(a, b, ...)` stops the macro from instantiating 32 | # corresponding declarations. Notice that other procs in 33 | # the typeclass might rely on the skipped ones, so be sure to 34 | # declare your implementations before instantiating a typeclass. 35 | # - `exporting(a, b, ...)` adds export marks to corresponding 36 | # proc declarations; `exporting(_)` exports all the defined procs. 37 | instance Monoid, string, skipping(mconcat) 38 | assert compiles(string.isMonoid) 39 | 40 | # We can leverage nim's concepts and type unions in our instances 41 | proc mempty[T: SomeNumber](_: typedesc[T]): T = T(0) 42 | proc mappend[T: SomeNumber](a, b: T): T = a + b 43 | instance Monoid, (T: SomeNumber) => T 44 | 45 | assert mconcat([1, 2, 3]) == 6 46 | assert mconcat([0.5, 0.5]) == 1.0 47 | 48 | # This means we can, to some extent, "derive" instances (if we have proper 49 | # concepts defined). 50 | type 51 | MonoidConcept = concept x 52 | # It was defined in typeclass 53 | isMonoid(type(x)) 54 | assert: string is MonoidConcept 55 | assert: not (bool is MonoidConcept) 56 | 57 | proc mempty[T: MonoidConcept](t: typedesc[Option[T]]): Option[T] = 58 | some(mempty(T)) 59 | proc mappend[T: MonoidConcept](a, b: Option[T]): Option[T] = 60 | if a.isNone: b 61 | elif b.isNone: a 62 | else: 63 | some(mappend(a.get, b.get)) 64 | instance Monoid, (T: MonoidConcept) => Option[T] 65 | assert: mconcat(@[some("foo"), some("bar")]) == some("foobar") 66 | # Works for Option[Option[string]] too! 67 | assert: mconcat(@[some(some("foo")), some(none(string)), some(some("bar"))]) == some(some("foobar")) 68 | 69 | # Everything up to this point, however, could be just as easily done 70 | # with generics: indeed, it would be enough to define `mconcat` 71 | # like this: 72 | # 73 | # .. code-block 74 | # 75 | # proc mconcat[T: MonoidConcept](a, b: T): T = ... 76 | # 77 | # and add corresponding `isMonoid` definitions. 78 | # 79 | # There is, however, something that is out of reach for Nim generics: 80 | # we can't abstract over type constructors. Classy was created for this 81 | # exact use case: 82 | 83 | typeclass Functor, F[_]: 84 | # Again, can't forward-declare this. 85 | # proc fmap[A, B](fa: F[A], g: A -> B): F[B] 86 | 87 | proc `$>`[A, B](fa: F[A], b: B): F[B] = 88 | fmap(fa, (a: A) => b) 89 | 90 | 91 | proc fmap[A, B](fa: Option[A], g: A -> B): Option[B] = 92 | fa.map(g) 93 | 94 | # Notice that `Option` is not a type: it is a type **constructor**. All 95 | # occurrences of the form `F[X]` in the typeclass body will be replaced 96 | # with `Option[X]`. 97 | instance Functor, Option[_] 98 | 99 | assert: (some("foo") $> 123) == some(123) 100 | 101 | # All previously mentioned features still work, so you can, for 102 | # example, write something like this: 103 | # 104 | # .. code-block 105 | # 106 | # instance Monad, A => Either[A, _] 107 | # 108 | # (after, of course, defining a suitable `Monad` typeclass) 109 | 110 | # We can also define typeclasses with multiple parameters: 111 | typeclass TraversableInst, [T[_], F[_]]: 112 | # proc traverse[A, B](ta: T[A], f: A -> F[B]): F[B] 113 | proc sequence[A](tfa: T[F[A]]): F[T[A]] = 114 | traverse(tfa, (fa: F[A]) => fa) 115 | 116 | # Notice that this is not `Traversable` in its proper form - we have 117 | # to use a separate instance for each applicative functor `F`. 118 | 119 | 120 | # We should use `Applicative` typeclass for this, but this is just an 121 | # example 122 | proc pure[A](ta: typedesc[Option[A]], a: A): Option[A] = some(a) 123 | proc `<*>`[A, B](fg: Option[A -> B], fa: Option[A]): Option[B] = 124 | if fg.isSome and fa.isSome: 125 | some(fg.get()(fa.get())) 126 | else: 127 | none(B) 128 | 129 | # We can define a `traverse` for `seq` and `Option` and be done with 130 | # it, but we'd have to duplicate the definition for each functor we want to 131 | # use. This does not seem pleasant. 132 | # 133 | # Let's try another approach: 134 | 135 | typeclass TraverseSeqWith, F[_]: 136 | proc traverse[A, B](ta: seq[A], f: A -> F[B]): F[seq[B]] = 137 | result = pure(F[seq[B]], newSeq[B]()) 138 | let worker: (seq[B] -> (B -> seq[B])) = (bs: seq[B]) => ((b: B) => bs & b) 139 | for a in ta: 140 | result = pure(F[type(worker)], worker) <*> result <*> f(a) 141 | 142 | instance TraversableInst, [seq[_], F[_]] 143 | 144 | # Now we only have to duplicate this line to support a new functor. 145 | instance TraverseSeqWith, Option[_] 146 | 147 | assert: sequence(@[1.some, 2.some, 3.some]) == some(@[1, 2, 3]) 148 | assert: sequence(@[1.some, none(int), 3.some]) == none(seq[int]) 149 | -------------------------------------------------------------------------------- /tests/test_classy.nim: -------------------------------------------------------------------------------- 1 | import unittest, future, sequtils, options, ../classy 2 | 3 | template shouldWork(body: untyped): untyped = 4 | when compiles(body): 5 | body 6 | else: 7 | check: compiles(body) 8 | 9 | template shouldFail(body: untyped): untyped = 10 | block: 11 | check: not compiles(body) 12 | 13 | suite "Typeclasses": 14 | test "Monoid": 15 | shouldWork: 16 | 17 | typeclass Monoid, M: 18 | # Can't use forward declarations for generics 19 | # proc mzero(t: typedesc[M]): M 20 | # proc mappend(a, b: M): M 21 | 22 | proc mconcat(ms: seq[M]): M = 23 | result = mzero(M) 24 | for m in ms: 25 | result = mappend(result, m) 26 | 27 | # Should work for one type 28 | proc mzero(t: typedesc[int]): int = 0 29 | proc mappend(a, b: int): int = a + b 30 | 31 | instance Monoid, int 32 | check: mconcat(@[1, 2, 3, 4]) == 10 33 | 34 | # Should allow more than one instance per typeclass 35 | proc mzero(t: typedesc[string]): string = "" 36 | proc mappend(a, b: string): string = a & b 37 | 38 | instance Monoid, string 39 | check: mconcat(@["foo", "bar"]) == "foobar" 40 | 41 | # Should allow parameterized instances 42 | proc mzero[A](t: typedesc[seq[A]]): seq[A] = @[] 43 | proc mappend[A](a, b: seq[A]): seq[A] = a & b 44 | 45 | instance Monoid, A => seq[A] 46 | check: mconcat(@[@[1, 2], @[3], @[4, 5]]) == @[1, 2, 3, 4, 5] 47 | 48 | # Should allow type constraints in parameters 49 | proc mzero[T: SomeNumber](t: typedesc[T]): T = 0 50 | proc mappend[T: SomeNumber](a, b: T): T = a + b 51 | 52 | instance Monoid, (T: SomeNumber) => T 53 | check: mconcat(@[0.5, 0.5]) == 1.0 54 | 55 | test "Functor": 56 | shouldWork: 57 | typeclass Functor, F[_]: 58 | # Can't use forward declarations for generics 59 | # proc fmap[A, B](fa: F[A], g: A -> B): F[B] 60 | 61 | proc `<$>`[A, B](g: A -> B, fa: F[A]): F[B] = fmap(fa, g) 62 | 63 | # Should allow type constructors 64 | proc fmap[A, B](fa: seq[A], g: A -> B): seq[B] = sequtils.map(fa, g) 65 | instance Functor, seq[_] 66 | 67 | check: ((x: int) => $x) <$> @[1, 2, 3] == @["1", "2", "3"] 68 | 69 | test "Monad": 70 | shouldWork: 71 | typeclass Monad, M[_]: 72 | # Can't use forward declarations for generics 73 | # proc point[A](t: typedesc[M[A]], v: A): M[A] 74 | # proc flatMap[A, B](ma: M[A], f: A -> M[B]): M[B] 75 | 76 | proc join[A](mma: M[M[A]]): M[A] = 77 | mma.flatMap((ma: M[A]) => ma) 78 | 79 | # Should allow instances for type constructors without patterns 80 | proc flatMap[A, B](ma: seq[A], f: A -> seq[B]): seq[B] = 81 | result = newSeq[B]() 82 | for a in ma: 83 | result.add(f(a)) 84 | 85 | instance Monad, seq[_] 86 | check: join(@[@[1, 2], @[3]]) == @[1, 2, 3] 87 | 88 | # Should allow mixing parameters with concrete types and wildcards 89 | # This also checks that macro correctly handles recursion in arguments 90 | proc flatMap[N, S, A, B](ma: (N, S, A), f: A -> (N, S, B)): (N, S, B) = 91 | let (n1, s1, a) = ma 92 | let (n2, s2, b) = f(a) 93 | (n1 + n2, s1 & s2, b) 94 | 95 | # Notice we don't reuse parameter names here - this is not yet supported 96 | instance Monad, N => (N, string, _) 97 | check: join((1, "foo", (2, "bar", 0.5))) == (3, "foobar", 0.5) 98 | 99 | test "Bifunctor": 100 | shouldWork: 101 | # Should allow multi-parameter patterns 102 | typeclass Bifunctor, F[_, _]: 103 | # proc bimap[A, B, C, D](f: F[A, B], g: A -> C, h: B -> D): F[C, D] 104 | proc first[A, B, C](f: F[A, B], g: A -> C): F[C, B] = 105 | f.bimap(g, (b: B) => b) 106 | proc second[A, B, C](f: F[A, B], h: B -> C): F[A, C] = 107 | f.bimap((a: A) => a, h) 108 | 109 | proc bimap[A, B, C, D](f: (A, B), g: A -> C, h: B -> D): (C, D) = 110 | (g(f[0]), h(f[1])) 111 | 112 | instance Bifunctor, (_, _) 113 | let t = (0.5.float, '2') 114 | .first(x => $x) 115 | .second(y => ord(y).int - ord('0')) 116 | check: t == ("0.5", 2) 117 | 118 | suite "Multi-parameter typeclasses": 119 | test "Should support multi-parameter typeclasses": 120 | shouldWork: 121 | typeclass Conversion, [A, B]: 122 | #proc to(a: A, tb: typedesc[B]): B 123 | proc mapTo(sa: seq[A], tb: typedesc[B]): seq[B] = 124 | sa.map(a => a.to(B)) 125 | 126 | proc to(x: int, tb: typedesc[string]): string = $x 127 | instance Conversion, [int, string] 128 | check: @[1, 2, 3].mapTo(string) == @["1", "2", "3"] 129 | 130 | test "Should check typeclass arity when instantiating": 131 | shouldWork: 132 | # Also checks few related features 133 | typeclass NoArgs, []: discard 134 | shouldWork: instance NoArgs, [] 135 | shouldFail: instance NoArgs, int 136 | shouldFail: instance NoArgs, [int] 137 | 138 | typeclass OneArg1, A: discard 139 | shouldFail: instance OneArg1, [] 140 | shouldWork: instance OneArg1, int 141 | shouldWork: instance OneArg1, [int] 142 | shouldFail: instance OneArg1, [int, string] 143 | 144 | typeclass OneArg2, [A]: discard 145 | shouldFail: instance OneArg2, [] 146 | shouldWork: instance OneArg2, int 147 | shouldWork: instance OneArg2, [int] 148 | shouldFail: instance OneArg2, [int, string] 149 | 150 | typeclass TwoArgs, [A, B]: discard 151 | shouldFail: instance TwoArgs, int 152 | shouldWork: instance TwoArgs, [int, string] 153 | shouldFail: instance TwoArgs, [int, string, float] 154 | 155 | test "Should only inject used parameters": 156 | type Identity[A] = object 157 | a: A 158 | 159 | shouldWork: 160 | typeclass A, [F, G]: 161 | proc foo(f: F) = discard 162 | instance A, (X, Y) => [Identity[X], Identity[Y]] 163 | foo[int](Identity[int](a: 123)) 164 | 165 | shouldWork: 166 | typeclass B, [F, G]: 167 | proc bar(f: F, g: G) = discard 168 | instance B, (X) => [Identity[X], X] 169 | bar[int]( 170 | Identity[int](a: 123), 171 | 123 172 | ) 173 | 174 | suite "Miscellaneous features": 175 | test "Skipping definitions": 176 | shouldWork: 177 | typeclass Some, S: 178 | proc foo: S = 1 179 | proc bar: S = 2 180 | proc `$%^`: S = 3 181 | 182 | block: 183 | instance Some, int, 184 | skipping(bar) 185 | 186 | check: declared(foo) 187 | check: not declared(bar) 188 | check: declared(`$%^`) 189 | 190 | block: 191 | instance Some, int, 192 | skipping(foo, bar, `$%^`) 193 | 194 | check: not declared(foo) 195 | check: not declared(bar) 196 | check: not declared(`$%^`) 197 | 198 | test "Template support": 199 | shouldWork: 200 | typeclass WithInverse, F: 201 | template inverse(f: F): F = -f 202 | instance WithInverse, int 203 | check: inverse(123) == -123 204 | check: not compiles(inverse(1.0)) 205 | 206 | test "Member parameters should not cause collision": 207 | shouldWork: 208 | typeclass TC, S: 209 | proc foo[A](a: A, s: S): (A, S) = 210 | (a, s) 211 | 212 | instance TC, A => Option[A] 213 | echo foo(123, some(true)) 214 | assert: foo(123, some(true)) == (123, some(true)) 215 | 216 | test "Should fail for constructor without arguments in body": 217 | shouldFail: 218 | typeclass Bad, B[_]: 219 | proc foo(x: B) = discard 220 | instance Bad, seq[_] 221 | 222 | test "Should properly inject outside proc definitions": 223 | shouldWork: 224 | typeclass Foo, F: 225 | let fooVal: F = 0 226 | 227 | instance Foo, int 228 | assert: fooVal == 0 229 | 230 | test "Should strip export markers on import": 231 | # With routines 232 | shouldWork: 233 | typeclass Foo, F: 234 | proc fooProc*: F = 0 235 | 236 | instance Foo, int 237 | 238 | # With operators 239 | shouldWork: 240 | typeclass Bar, F: 241 | proc `$@`*: F = 0 242 | 243 | instance Bar, int 244 | 245 | # With variables 246 | shouldWork: 247 | typeclass Baz, F: 248 | let bazVal*: F = 0 249 | 250 | instance Baz, int 251 | 252 | test "Shouldn't allow incorrect instance options": 253 | typeclass Foo, F: 254 | discard 255 | 256 | shouldFail: 257 | instance Foo, int, invalid() 258 | -------------------------------------------------------------------------------- /classy.nim: -------------------------------------------------------------------------------- 1 | import macros, future, intsets 2 | from sequtils import apply, map, zip, toSeq, applyIt, allIt, mapIt 3 | 4 | ## Classy 5 | ## ====== 6 | ## 7 | ## Provides ability to define and instantiate haskell-like typeclasses in Nim. 8 | ## 9 | ## Overview 10 | ## -------- 11 | ## 12 | ## This module consists of two macros. ``typeclass`` saves a parameterized AST to 13 | ## serve as a template (in an object of type ``Typeclass``). ``instance`` performs 14 | ## necessary substitutions in saved AST for provided arguments, and executes 15 | ## the resulting code. 16 | ## 17 | ## As an example, let's say we want to define a ``Functor`` typeclass: 18 | ## 19 | ## .. code-block:: nim 20 | ## typeclass Functor, F[_]: 21 | ## # proc fmap[A, B](fa: F[A], g: A -> B): F[B] 22 | ## proc `$>`[A, B](fa: F[A], b: B): F[B]= 23 | ## fmap(fa, (a: A) => b) 24 | ## 25 | ## This code does not declare any procs - only a compile-time variable ``Functor``. 26 | ## Notice that our code abstracts over type constructor - ``F`` is just a 27 | ## placeholder. 28 | ## 29 | ## Notice that ``fmap`` is not part of the typeclass. At the moment forward 30 | ## declarations don't work for generic procs in Nim. As we'll see, even if 31 | ## proc AST in our typeclass has no generic parameters, the generated proc 32 | ## can have some. So it is recommended to not define unimplemented procs 33 | ## inside typeclasses. This will probably change after this issue is closed: 34 | ## https://github.com/nim-lang/Nim/issues/4104. 35 | ## 36 | ## Now let's instantiate our typeclass. For example, let's define a ``Functor`` 37 | ## instance for all tuples of two elements, where the left value is ``SomeInteger``: 38 | ## 39 | ## .. code-block:: nim 40 | ## 41 | ## instance Functor, (C: SomeInteger) => (C, _) 42 | ## 43 | ## This generates the following proc definition: 44 | ## 45 | ## .. code-block:: nim 46 | ## 47 | ## proc `$>`[A, B, C: SomeInteger](fa: (C, A), b: B): (C, B)= 48 | ## fmap(fa, (a: A) => b) 49 | ## 50 | ## Here are the few things to notice: 51 | ## 52 | ## 1. All ocurrences of form ``F[X]`` were replaced with ``(C, X)`` 53 | ## 2. ``C``, the parameter of our instance, became the parameter of the generated 54 | ## definition, with its constraint preserved. 55 | ## 3. We're referring to a proc ``fmap``. So any procs, that are assumed to be 56 | ## present in typeclass body, should be defined with corresponding types 57 | ## before the typeclass is instantiated 58 | 59 | type 60 | Unit = tuple[] 61 | 62 | AbstractPattern = object 63 | ## Type/pattern placeholder for typeclass declaration. 64 | ## 65 | ## Has either form ``A`` (placeholder for a concrete type) or ``A[_,...]`` (for 66 | ## a type constructor). 67 | ## Doesn't have to evaluate to concrete type after parameter substitution - 68 | ## must be eliminated after typeclass instantiation. 69 | ident: NimIdent 70 | arity: Natural 71 | ## types with arity of zero are considered concrete, with corresponding 72 | ## matching rules 73 | 74 | ConcretePattern = object 75 | ## A tree with zero or more wildcards (_). 76 | ## 77 | ## Should evaluate to concrete type once all the wildcards are replaced with 78 | ## types. 79 | tree: NimNode 80 | arity: Natural 81 | 82 | Typeclass* = object 83 | # Only here for better error messaging. 84 | # Do not use for membership checks and such! 85 | name: string 86 | patterns: seq[AbstractPattern] 87 | body: NimNode 88 | 89 | TypeclassMember = object 90 | patterns: seq[ConcretePattern] 91 | params: seq[NimNode] 92 | 93 | ExportOptionsKind = enum eoNone, eoSome, eoAll 94 | ExportOptions = object 95 | case kind: ExportOptionsKind 96 | of {eoNone, eoAll}: discard 97 | of eoSome: 98 | patterns: seq[NimNode] 99 | 100 | MemberOptions = object 101 | # Idents of the top-level procs to be skipped 102 | skipping: seq[NimNode] 103 | exporting: ExportOptions 104 | 105 | TypeclassOptions = object 106 | exported: bool 107 | 108 | # https://github.com/nim-lang/Nim/issues/4952 109 | GenTransformTuple[S] = object 110 | newNode: NimNode 111 | recurse: bool 112 | state: S 113 | 114 | TransformTuple = GenTransformTuple[Unit] 115 | 116 | const unit: Unit = () 117 | 118 | proc mkGenTransformTuple[S](newNode: NimNode, recurse: bool, state: S): auto = 119 | GenTransformTuple[S](newNode: newNode, recurse: recurse, state: state) 120 | 121 | proc mkTransformTuple(newNode: NimNode, recurse: bool): TransformTuple = 122 | mkGenTransformTuple(newNode, recurse, unit) 123 | 124 | template fail(msg: string, n: NimNode = nil) = 125 | let errMsg = if n != nil: msg & ": " & $toStrLit(n) else: msg 126 | when compiles(error("", nil.NimNode)): 127 | error(errMsg, n) 128 | else: 129 | # Nim 0.15.2 and older 130 | error(errMsg) 131 | 132 | proc arity(x: Typeclass): Natural {.compileTime.} = 133 | x.patterns.len 134 | 135 | proc arity(x: TypeclassMember): Natural {.compileTime.} = 136 | x.patterns.len 137 | 138 | proc replaceInBody( 139 | tree: NimNode, 140 | substs: seq[(AbstractPattern, ConcretePattern)], 141 | substIndices: IntSet 142 | ): tuple[tree: NimNode, substIndices: IntSet] 143 | 144 | proc transformDown[S]( 145 | tree: NimNode, 146 | f: (n: NimNode, s: S) -> GenTransformTuple[S], 147 | s: S 148 | ): (NimNode, S) {.compileTime.} = 149 | let tup = f(tree.copyNimTree, s) 150 | var resNode = tup.newNode 151 | var resState = tup.state 152 | if tup.recurse: 153 | for i in 0.. TransformTuple 163 | ): NimNode {.compileTime.} = 164 | proc fs(n: NimNode, s: Unit): GenTransformTuple[Unit] {.closure.} = 165 | f(n) 166 | transformDown[tuple[]]( 167 | tree = tree, 168 | f = fs, 169 | s = unit 170 | )[0] 171 | 172 | proc asTree(p: AbstractPattern): NimNode {.compileTime.} = 173 | ## Restore `p`'s tree form 174 | ## 175 | ## Only useful for error messages - use fields for matching. 176 | if p.arity == 0: 177 | result = newIdentNode(p.ident) 178 | else: 179 | result = newTree( 180 | nnkBracketExpr, 181 | newIdentNode(p.ident) 182 | ) 183 | for i in 1..p.arity: 184 | result.add(newIdentNode("_")) 185 | 186 | proc getArity(tree: NimNode): int {.compileTime.} = 187 | ## Counts all underscore idents in ``tree`` 188 | if tree.eqIdent("_"): 189 | result = 1 190 | else: 191 | result = 0 192 | for child in tree: 193 | result.inc(getArity(child)) 194 | 195 | proc matchesPattern( 196 | tree: NimNode, pattern: AbstractPattern 197 | ): bool {.compileTime.} = 198 | ## Checks whether ``tree`` is an occurence of ``pattern`` 199 | ## 200 | ## Returns ``true`` if the patern matches, ``false`` otherwise. 201 | ## Raises if the arity does not match! 202 | if tree.eqIdent($pattern.ident): 203 | 204 | # TODO: This should happen at instantiation! 205 | # We should not allow invalid class body. 206 | if pattern.arity > 0: 207 | fail("Constructor pattern cannot be used without arguments", tree) 208 | 209 | # Concrete type - does not require brackets 210 | true 211 | 212 | elif tree.kind == nnkBracketExpr and 213 | tree.len > 0 and 214 | tree[0].eqIdent($pattern.ident): 215 | # Constructor - check arity 216 | let arity = tree.len - 1 217 | if arity != pattern.arity: 218 | let msg = "Wrong number of type arguments in expression " & 219 | "(expected " & $pattern.arity & ")" 220 | fail(msg, tree) 221 | 222 | true 223 | else: 224 | false 225 | 226 | proc instantiateConstructor( 227 | concrete: ConcretePattern, abstract: AbstractPattern, tree: NimNode, 228 | processParam: NimNode -> NimNode 229 | ): NimNode {.compileTime.} = 230 | 231 | proc replaceUnderscores( 232 | tree: NimNode, args: seq[NimNode] 233 | ): (NimNode, seq[NimNode]) = 234 | var argsNew = args 235 | # Traverse ``tree`` and replace all underscore identifiers 236 | # with nodes from ``args`` in order. 237 | let treeNew = transformDown(tree) do (sub: NimNode) -> auto: 238 | if sub.eqIdent("_"): 239 | let res = argsNew[0].copyNimTree 240 | argsNew.delete(0) 241 | mkTransformTuple(res, false) 242 | else: 243 | mkTransformTuple(sub, true) 244 | 245 | (treeNew, argsNew) 246 | 247 | tree.expectKind(nnkBracketExpr) 248 | # First one is the constructor itself 249 | var args = toSeq(tree.children) 250 | args.delete(0) 251 | 252 | # we can have recursion in type arguments! 253 | args.apply(processParam) 254 | 255 | (result, args) = replaceUnderscores(concrete.tree, args) 256 | doAssert: args.len == 0 257 | 258 | proc instantiate( 259 | concrete: ConcretePattern, abstract: AbstractPattern, tree: NimNode, 260 | processParam: NimNode -> NimNode 261 | ): NimNode {.compileTime.} = 262 | if abstract.arity > 0: 263 | concrete.instantiateConstructor(abstract, tree, processParam) 264 | else: 265 | # Members without parameters do not have brackets 266 | concrete.tree.copyNimTree 267 | 268 | 269 | when declared(nnkTupleConstr): 270 | const TupleConstrKinds = {nnkPar, nnkTupleConstr} 271 | else: 272 | const TupleConstrKinds = {nnkPar} 273 | 274 | proc parseMemberParams( 275 | tree: NimNode 276 | ): seq[NimNode] = 277 | ## parse instance parameters in following forms: 278 | ## ``A``, ``(A, B)``, ``(A: T1, B)`` etc. 279 | 280 | if tree.kind in TupleConstrKinds: 281 | result = toSeq(tree.children) 282 | else: 283 | result = @[tree] 284 | 285 | for i in 0.. 1 and 300 | tree[0].kind == nnkIdent and 301 | (toSeq(tree.children))[1..tree.len-1].allIt(it == wildcard) 302 | ) 303 | 304 | if not isValid: 305 | fail("Illegal typeclass parameter expression", tree) 306 | 307 | if tree.kind == nnkBracketExpr: 308 | AbstractPattern( 309 | ident: tree[0].ident, 310 | arity: tree.len - 1 311 | ) 312 | else: 313 | AbstractPattern(ident: tree.ident, arity: 0) 314 | 315 | proc parseAbstractPatterns( 316 | tree: NimNode 317 | ): seq[AbstractPattern] {.compileTime.} = 318 | let patternNodes = (block: 319 | if tree.kind == nnkBracket: 320 | toSeq(tree.children) 321 | else: 322 | @[tree] 323 | ) 324 | patternNodes.map(parseAbstractPattern) 325 | 326 | proc replace(n: NimNode, subst: seq[(NimNode, NimNode)]): NimNode {.compileTime.} = 327 | transformDown(n) do (sub: NimNode) -> auto: 328 | for pair in subst: 329 | if sub == pair[0]: 330 | return mkTransformTuple(pair[1], false) 331 | return mkTransformTuple(sub, true) 332 | 333 | proc containsSubtree(n: NimNode, sub: NimNode): bool = 334 | var q = @[n] 335 | while q.len > 0: 336 | let cur = q.pop 337 | if cur == sub: 338 | return true 339 | else: 340 | for child in cur: 341 | q.add(child) 342 | 343 | return false 344 | 345 | # Ugly workaround for Nim bug: 346 | # https://github.com/nim-lang/Nim/issues/4939 347 | # TODO: remove this the second the bug is fixed 348 | var cnt {.compileTime.} = 0 349 | proc genIdent( 350 | s: string 351 | ): NimNode {.compileTime.} = 352 | inc cnt 353 | newIdentNode(s & "_classy_" & $cnt) 354 | 355 | proc genSymParams( 356 | inParams: seq[NimNode], 357 | inPattern: NimNode 358 | ): tuple[params: seq[NimNode], pattern: NimNode] {.compileTime.} = 359 | ## Replace instance parameters with unique symbols 360 | var substitutions = newSeq[(NimNode, NimNode)]() 361 | 362 | result.params = inParams 363 | for i in 0.. [Constr[A, _, ...], Concrete, ...]`` 380 | ## - ``(A: T1, B..) => Constr[A, _, ...]`` 381 | ## - ``A => Constr[A, _, ...]`` 382 | ## - ``Constr[_, ...]`` 383 | ## - ``Concrete`` 384 | let hasParams = tree.kind == nnkInfix and tree[0].eqIdent("=>") 385 | let (params0, pattern0) = (block: 386 | if hasParams: 387 | (parseMemberParams(tree[1]), tree[2]) 388 | else: 389 | (@[], tree) 390 | ) 391 | 392 | # Make sure parameters don't clash with anything in body 393 | let (params, patternsTree) = genSymParams(params0, pattern0) 394 | 395 | # Strip possible brackets around patterns 396 | let patternNodes = (block: 397 | if patternsTree.kind == nnkBracket: 398 | toSeq(patternsTree.children) 399 | else: 400 | @[patternsTree] 401 | ) 402 | let patterns = patternNodes.map(n => 403 | ConcretePattern(tree: n, arity: getArity(n)) 404 | ) 405 | 406 | TypeclassMember( 407 | params: params, 408 | patterns: patterns 409 | ) 410 | 411 | proc stripAccQuoted(n: NimNode): NimNode = 412 | case n.kind: 413 | of nnkAccQuoted: n[0] 414 | else: n 415 | 416 | proc parseMemberOptions( 417 | args: seq[NimNode] 418 | ): MemberOptions = 419 | ## Parse following instance options: 420 | ## - ``skipping(foo)`` - skips ``foo`` definition 421 | ## - ``skipping(foo, bar)`` - skips ``foo`` and ``bar`` 422 | ## - ``exporting(_)`` - export all symbols 423 | ## - ``exporting(foo, bar)`` - export ``foo`` and ``bar`` 424 | 425 | result = MemberOptions( 426 | skipping: newSeq[NimNode](), 427 | exporting: ExportOptions(kind: eoNone) 428 | ) 429 | for a in args: 430 | if a.kind == nnkCall and a[0].eqIdent("skipping"): 431 | # Don't check for duplicate symbols 432 | for i in 1.. 2: 443 | # Can't mix wildcard with other exporting 444 | fail("Invalid exporting clause", a) 445 | acc.add(stripAccQuoted(a[i])) 446 | 447 | if acc.len == 1 and acc[0].eqIdent("_"): 448 | result.exporting = ExportOptions(kind: eoAll) 449 | else: 450 | result.exporting = ExportOptions(kind: eoSome, patterns: acc) 451 | 452 | else: 453 | fail("Invalid instance option", a) 454 | 455 | proc parseTypeclassOptions( 456 | args: seq[NimNode] 457 | ): TypeclassOptions = 458 | result = TypeclassOptions(exported: false) 459 | for a in args: 460 | if a.eqIdent("exported"): 461 | if result.exported: 462 | fail("Duplicate exported clause", a) 463 | else: 464 | result.exported = true 465 | else: 466 | fail("Illegal typeclass option: ", a) 467 | 468 | # Global for reuse in ``replaceInProcs`` 469 | proc mkBodyWorker( 470 | substs: seq[(AbstractPattern, ConcretePattern)] 471 | ): (n: NimNode, s: IntSet) -> GenTransformTuple[IntSet] = 472 | proc worker(sub: NimNode, substIndices0: IntSet): GenTransformTuple[IntSet] = 473 | var substIndices = substIndices0 474 | for ix, subst in substs: 475 | let (abstract, concrete) = subst 476 | if sub.matchesPattern(abstract): 477 | substIndices.incl(ix) 478 | 479 | proc processParam(n: NimNode): NimNode = 480 | let replaced = n.replaceInBody(substs, substIndices) 481 | substIndices = replaced.substIndices 482 | replaced.tree 483 | 484 | let newSub = concrete.instantiate( 485 | abstract, 486 | sub, 487 | processParam 488 | ) 489 | return mkGenTransformTuple(newSub, false, substIndices) 490 | 491 | return mkGenTransformTuple(sub.copyNimTree, true, substIndices) 492 | 493 | worker 494 | 495 | proc replaceInBody( 496 | tree: NimNode, 497 | substs: seq[(AbstractPattern, ConcretePattern)], 498 | substIndices: IntSet 499 | ): tuple[tree: NimNode, substIndices: IntSet] = 500 | ## Replace ``substs`` in a tree. 501 | ## Add indices of any matching substs to `substIndices` 502 | transformDown[IntSet]( 503 | tree, 504 | mkBodyWorker(substs), 505 | substIndices 506 | ) 507 | 508 | proc processProcParams( 509 | paramsTree: NimNode, 510 | substs: seq[(AbstractPattern, ConcretePattern)] 511 | ): tuple[tree: NimNode, substIndices: IntSet] = 512 | paramsTree.expectKind(nnkFormalParams) 513 | var substIndices = initIntSet() 514 | proc processType(tree: NimNode, substIndices: var IntSet): NimNode = 515 | let res = tree.replaceInBody(substs, substIndices) 516 | substIndices = res.substIndices 517 | res.tree 518 | 519 | var res = newNimNode(nnkFormalParams) 520 | 521 | res.add(paramsTree[0].processType(substIndices)) 522 | 523 | for i in 1.. 0: 561 | genParams = newNimNode(nnkGenericParams) 562 | 563 | var instanceParamIndices = initIntSet() 564 | for i, param in instanceParams: 565 | for j, subst in substs: 566 | if j in substIndices1 and 567 | subst[1].tree.containsSubtree(param[0]): 568 | instanceParamIndices.incl(i) 569 | break 570 | 571 | for i in instanceParamIndices: 572 | genParams.add(instanceParams[i].copyNimTree) 573 | 574 | res[2] = genParams 575 | 576 | # Do not recurse - we already replaced everything using ``replaceInBody`` 577 | mkTransformTuple(res, false) 578 | else: 579 | # Note that arguments of a replaced constructor are handled by 580 | # ``bodyWorker``, meaning any proc defs inside them don't get parameter 581 | # injection. This is probably the right thing to do, though. 582 | let substIndices = initIntSet() 583 | let genTup = mkBodyWorker(substs)(sub, substIndices) 584 | mkTransformTuple(genTup.newNode, true) 585 | 586 | transformDown(tree, worker) 587 | 588 | proc removeSkippedProcs( 589 | tree: NimNode, 590 | skipping: seq[NimNode] 591 | ): NimNode {.compileTime.} = 592 | ## Traverse ``tree`` looking for top-level procs with names 593 | ## in ``skipping`` and remove their definitions. 594 | 595 | proc worker(sub: NimNode): TransformTuple = 596 | case sub.kind 597 | of RoutineNodes: 598 | let nameNode = stripAccQuoted(sub.name) 599 | if nameNode in skipping: 600 | mkTransformTuple(newEmptyNode(), false) 601 | else: 602 | mkTransformTuple(sub, false) 603 | else: 604 | mkTransformTuple(sub, true) 605 | 606 | transformDown(tree, worker) 607 | 608 | proc addExportMarks( 609 | tree: NimNode, 610 | exporting: ExportOptions 611 | ): NimNode {.compileTime.} = 612 | proc contains(opts: ExportOptions, n: NimNode): bool = 613 | case opts.kind 614 | of eoNone: false 615 | of eoAll: true 616 | of eoSome: opts.patterns.contains(n) 617 | 618 | proc stripExportMark(n: NimNode): NimNode = 619 | if n.kind == nnkPostfix: n.basename else: n 620 | 621 | proc withExportMark(n: NimNode, mark: bool): NimNode = 622 | if mark: n.postfix("*") else: n 623 | 624 | proc worker(sub: NimNode): TransformTuple = 625 | case sub.kind 626 | of RoutineNodes: 627 | let exported = exporting.contains(sub.name) 628 | let res = sub.copyNimTree 629 | # Add or remove export mark 630 | # `name` proc strips quoting and postfixes - but we need them! 631 | res[0] = sub[0].stripExportMark.withExportMark(exported) 632 | # We're only processing top-level routines 633 | mkTransformTuple(res, false) 634 | 635 | of nnkIdentDefs: 636 | let name = sub[0].stripExportMark 637 | let exported = exporting.contains(name) 638 | let res = sub.copyNimTree 639 | res[0] = name.withExportMark(exported) 640 | # We're only processing top-level definitions 641 | # TODO: handle exports for object fields 642 | mkTransformTuple(res, false) 643 | 644 | else: 645 | mkTransformTuple(sub, true) 646 | 647 | transformDown(tree, worker) 648 | 649 | proc instanceImpl( 650 | class: Typeclass, 651 | member: TypeclassMember, 652 | options: MemberOptions 653 | ): NimNode {.compileTime.} = 654 | 655 | if class.arity != member.arity: 656 | let msg = "Incorrect number of arguments for typeclass " & class.name 657 | fail(msg) 658 | 659 | let substs: seq[(AbstractPattern, ConcretePattern)] = 660 | class.patterns.zip(member.patterns) 661 | 662 | for s in substs: 663 | let (abstract, concrete) = s 664 | if abstract.arity != concrete.arity: 665 | let msg = "Type or constructor does not match typeclass parameter (" & 666 | $toStrLit(asTree(abstract)) & ")" 667 | fail(msg, concrete.tree) 668 | 669 | result = class.body.copyNimTree 670 | result = result.removeSkippedProcs(options.skipping) 671 | result = result.replaceInProcs(member.params, substs) 672 | result = result.addExportMarks(options.exporting) 673 | 674 | # A hack to allow passing ``Typeclass`` values from the macro to 675 | # defined variables 676 | var tc {.compiletime.} : Typeclass 677 | 678 | macro typeclass*(id, patternsTree: untyped, args: varargs[untyped]): untyped = 679 | ## Define typeclass with name ``id``. 680 | ## 681 | ## This creates a compile-time variable with name ``id``. 682 | ## 683 | ## Call syntax: 684 | ## 685 | ## .. code-block:: nim 686 | ## 687 | ## typeclass Class, [A[_, ..], B,...], exported: 688 | ## 689 | ## 690 | ## Typeclass can have zero or more parameters, each of which can be a type 691 | ## constructor with arity 1 and higher (like ``A`` in sample above), or be 692 | ## a concrete type (like ``B``). If a typeclass has exactly one parameter, 693 | ## the brackets around parameter list can be omitted. 694 | ## 695 | ## The ``exported`` option allows to export the typeclass from module. 696 | ## This marks corresponding variable with export postfix. Notice that in 697 | ## this case standard restrictions apply: the ``typeclass`` call should be 698 | ## in module's top scope. 699 | id.expectKind(nnkIdent) 700 | 701 | # Typeclass body goes last, before it - various options 702 | let argsSeq = toSeq(args) 703 | if argsSeq.len == 0: 704 | fail("Missing body for typeclass" & $id) 705 | let options = parseTypeclassOptions(argsSeq[0..^2]) 706 | let body = argsSeq[argsSeq.len - 1] 707 | let patterns = parseAbstractPatterns(patternsTree) 708 | 709 | let idTree = if options.exported: id.postfix("*") else: id 710 | 711 | # Pass the value through ``tc``. 712 | # I do not know of a cleaner way to do this. 713 | tc = Typeclass( 714 | name: $id, 715 | patterns: patterns, 716 | body: body 717 | ) 718 | let tcSym = bindSym("tc") 719 | quote do: 720 | let `idTree` {.compileTime.} = `tcSym` 721 | 722 | macro instance*( 723 | class: static[Typeclass], 724 | argsTree: untyped, 725 | options: varargs[untyped] 726 | ): untyped = 727 | ## Instantiate typeclass ``class`` with given arguments 728 | ## 729 | ## Call syntax: 730 | ## 731 | ## .. code-block:: nim 732 | ## 733 | ## instance Class, (K, L) => [AType[_, K,..], BType,...], 734 | ## skipping(foo, bar), 735 | ## exporting(_) 736 | ## 737 | ## ``instance`` does the following: 738 | ## 1. Replaces each of parameter forms in `typeclass` definition with 739 | ## corresponding argument form (like ``AType`` in code example). Parameter 740 | ## form list of ``typeclass`` and argument form lists of corresponding 741 | ## `instance` calls must have matching length, and corresponding forms 742 | ## should have the same arity. 743 | ## 2. Injects instance parameters (``K`` in the example) into top-level 744 | ## routines in typeclass body. 745 | ## 3. Transforms the body according to options. 746 | ## 4. Executes the body. 747 | ## 748 | ## Instance `parameters` can have constraints in the same form as in a generic 749 | ## definition. If no instance parameters are present, the corresponding list 750 | ## can be omitted. If exactly one instance parameter is present (without 751 | ## constraints), the parenthesis around parameter list can be omitted. 752 | ## 753 | ## Instance `argument` forms are trees with zero or more nodes replaced with 754 | ## wildcards (``_``), the number of wildcards in a tree corresponding to 755 | ## the form arity. A form can include existing types, as well as instance 756 | ## `parameters`. 757 | ## 758 | ## Here are few valid parameter-argument combinations: 759 | ## 760 | ## .. code-block:: nim 761 | ## 762 | ## [] 763 | ## int 764 | ## Option[_] 765 | ## [int, string] 766 | ## K => Option[K] 767 | ## (K: SomeInteger, L) => [(K, L, Option[_]), (L, int)] 768 | ## 769 | ## Supported instance options: 770 | ## 1. ``skipping(foo, bar...)`` - these definitions will be skipped. 771 | ## 2. ``exporting(foo, bar)`` - these generated definitions will have export 772 | ## marker. 773 | ## 3. ``exporting(_)`` - all generated definitions will have export marker. 774 | 775 | var opts = newSeq[NimNode]() 776 | for o in options: opts.add(o) 777 | 778 | result = instanceImpl(class, parseMember(argsTree), parseMemberOptions(opts)) 779 | 780 | # For debugging purposes 781 | when defined(classyDumpCode): 782 | echo toStrLit(result) 783 | when defined(classyDumpTree): 784 | echo treeRepr(result) 785 | --------------------------------------------------------------------------------