├── .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 | [](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 |
--------------------------------------------------------------------------------