Skip to content

Commit ba10d7f

Browse files
committed
Generics Traits
1 parent c94c9de commit ba10d7f

File tree

1 file changed

+296
-0
lines changed

1 file changed

+296
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Generics Traits
2+
3+
* Proposal: [ME-0004](https://github.com/illusory0x0/moonbit-evolution/blob/0004-generics-traits/proposals/0004-generics-traits.mbt.md)
4+
* Author: [illusory0x0](https://github.com/illusory0x0)
5+
* Status: Under review
6+
* Review and discussion: [Github issue](https://github.com/moonbitlang/moonbit-evolution/pull/5)
7+
8+
## Introduction
9+
10+
We use type to constrain the value, and trait to constrain the type, but now supports generic data structures, but not generic traits which leads to a lot of API consistency issues when using generic data structures.
11+
12+
On the other hand, JavaScript has many APIs that require generic traits, the underlying DOM, and other web front-end frameworks make heavy use of generic classes, such as `react.js`, `vue.js`.
13+
14+
Although using `cast` function can solve some problems, it is not good practice to use `cast` function heavily.
15+
16+
17+
## Motivation
18+
19+
### Check API consistency
20+
21+
Right now Moonbit's trait doesn't support generic parameters,
22+
which can lead to a lot of API inconsistencies when using generic data structures.
23+
24+
for example,
25+
[Moonbit core library](https://github.com/moonbitlang/core/issues?q=is%3Aissue%20label%3A%22consistency%20review%22) has
26+
**29** consistency review issues, if we have **Generics Traits**, the compiler can check API consistency without human to code review.
27+
28+
### Improving JavaScript API Interoperability
29+
30+
Many JavaScript APIs use [Generic Classes](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-classes)
31+
or [Generic Interfaces](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-types).
32+
33+
Using the most commonly used API `querySelector` as an example. We have to write `DOM_Element::downcast` to cast.
34+
35+
[rescript webapi](https://github.com/TheSpyder/rescript-webapi/) using sophisticated techniques to emulate subtying and inheritance.
36+
37+
[purescript-dom-classy](https://pursuit.purescript.org/packages/purescript-dom-classy/) using typeclass to make DOM API binding more ergonomic.
38+
39+
[jsoo dom](https://github.com/ocsigen/js_of_ocaml/blob/master/lib/js_of_ocaml/dom.ml) using OCaml object inheritance
40+
to implement DOM API binding whose experience is closest to JavaScript.
41+
42+
Moonbit's type system is much closer to PureScript,
43+
and the experience with traits/typeclass is still quite good,
44+
so it was necessary to enhance the ability of the trait.
45+
46+
### Improve performance via avoiding dynamic dispatch
47+
48+
Now that the `Show` trait calls the `&Logger` trait object using dynamic dispatch can affect performance.
49+
50+
```moonbit
51+
pub(open) trait Show {
52+
output(Self, &Logger) -> Unit
53+
to_string(Self) -> String = _
54+
}
55+
```
56+
57+
58+
```moonbit
59+
///|
60+
#external
61+
type Element
62+
63+
///|
64+
trait DOM_Element {
65+
querySelector(Self, String) -> Element = _
66+
downcast(Element) -> Self = _
67+
to_element(Self) -> Element
68+
}
69+
70+
///|
71+
extern "js" fn Element::querySelector(
72+
self : Element,
73+
selectors : String
74+
) -> Element = "(self,selector) => self.querySelector(selector)"
75+
76+
///|
77+
fn[A, B] coerce(x : A) -> B = "%identity"
78+
79+
///|
80+
impl DOM_Element with querySelector(self, selectors) {
81+
Element::querySelector(coerce(self), selectors)
82+
}
83+
84+
///|
85+
impl DOM_Element with downcast(self) = "%identity"
86+
87+
///|
88+
#external
89+
type HTMLCanvasElement
90+
91+
///|
92+
#external
93+
type HTMLDivElement
94+
95+
///|
96+
#external
97+
type HTMLAnchorElement
98+
99+
///|
100+
#external
101+
type HTMLImageElement
102+
103+
///|
104+
impl DOM_Element for HTMLCanvasElement with to_element(self) = "%identity"
105+
106+
///|
107+
impl DOM_Element for HTMLDivElement with to_element(self) = "%identity"
108+
109+
///|
110+
impl DOM_Element for HTMLAnchorElement with to_element(self) = "%identity"
111+
112+
///|
113+
impl DOM_Element for HTMLImageElement with to_element(self) = "%identity"
114+
115+
///|
116+
test {
117+
let x : HTMLDivElement = {
118+
...
119+
}
120+
let e : HTMLImageElement = x.querySelector("x") |> DOM_Element::downcast
121+
122+
}
123+
```
124+
125+
126+
127+
128+
## Proposed solution
129+
130+
131+
### Functor Example
132+
133+
134+
This makes it easy to use the compiler to check for API consistency.
135+
136+
Actually this `Functor` more similar to [traverse](https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelude.html#v:traverse) when passing callback function which raise error.
137+
138+
```moonbit
139+
trait Functor[A] {
140+
fn[B] map(self : Self[A], f : (A) -> B raise?) -> Self[B] raise?
141+
}
142+
143+
trait Monad[A] : Functor[A] {
144+
fn[B] bind(self : Self[A], f : (A) -> Self[B] raise?) -> Self[B] raise?
145+
pure(x : A) -> Self[A]
146+
}
147+
```
148+
149+
without type annotation impl.
150+
151+
```moonbit
152+
impl Functor for FixedArray with map(self, f) { ... }
153+
impl Monad for FixedArray with bind(self, f) { ... }
154+
impl Monad for FixedArray with pure(a) { ... }
155+
156+
impl Functor for Result with map(self, f) { ... }
157+
impl Monad for Result with bind(self, f) { ... }
158+
impl Monad for Result with pure(a) { ... }
159+
```
160+
161+
with type annotation impl.
162+
163+
```moonbit
164+
impl Functor for FixedArray with[A,B] map(self : Self[A], f : (A) -> B raise?) -> Self[B] raise? { ... }
165+
impl Monad for FixedArray with[A,B] bind(self : Self[A], f : (A) -> Self[B] raise?) -> Self[B] raise? { ... }
166+
impl Monad for FixedArray with[A] pure(x : A) -> Self[A] { ... }
167+
168+
impl Functor for Result with[A,B] map(self : Self[A], f : (A) -> B raise?) -> Self[B] raise? { ... }
169+
impl Monad for Result with[A,B] bind(self : Self[A], f : (A) -> Self[B] raise?) -> Self[B] raise? { ... }
170+
impl Monad for Result with[A] pure(x : A) -> Self[A] { ... }
171+
```
172+
173+
174+
with generic traits, we can list the list of such traits that types are meant to belong, and thus improve API consistency.
175+
176+
### Iterable Example
177+
178+
We can define `Iterable` trait to `derive` something like `C#` [IEnumerable<T>](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1?view=net-9.0) or `Haskell` [Foldable](https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelude.html#t:Foldable) reduce many boilerplates and keep API consistency.
179+
180+
Use traits as API design guidelines, just as we did with traits that weren't generalized before.
181+
182+
C++ using [Range Concepts](https://en.cppreference.com/w/cpp/header/ranges.html#Concepts) and [Iterator Concepts](https://en.cppreference.com/w/cpp/iterator.html) as API design guidelines, If someone implement this convention, then they enjoy the benefits of [algorithm](https://en.cppreference.com/w/cpp/algorithm.html) which is common data structures operation.
183+
184+
```moonbit
185+
186+
trait Iterable[A] {
187+
iter(Self[A]) -> Iter[A]
188+
each(Self[A], f : (A) -> Unit raise?) -> Unit raise? = _
189+
eachi(Self[A], f : (Int,A) -> Unit raise?) -> Unit raise? = _
190+
fn[S] fold(Self[A], init~ : S, f : (S,A) -> S raise?) -> S raise? = _
191+
find_first(Self[A], f : (A) -> Bool) -> T? = _
192+
contains(Self[A],value : A) -> Bool = _
193+
}
194+
195+
trait KnownSizedIterable[A] : Iterable[A] {
196+
size(Self[A]) -> Int
197+
collect(Self[A]) -> Array[A] = _
198+
// This performance is better than `Iter::collect`
199+
to_array(Self[A]) -> Array[A] = _
200+
to_fixedarray(Self[A]) -> FixedArray[A] = _
201+
202+
}
203+
204+
```
205+
206+
we also can improve performance for `Array::push_iter`.
207+
208+
```moonbit
209+
fn[A,I : KnownSizedIterable] Array::push_iter(self : Self[A], xs : I[A]) -> Unit {
210+
self.reserve_capacity(self.length() + xs.size())
211+
for x in xs {
212+
self.push(x)
213+
}
214+
}
215+
```
216+
217+
### querySelector Example
218+
219+
support generics method in traits can reduce many `downcast` call.
220+
221+
```moonbit
222+
///|
223+
#external
224+
type Element
225+
226+
///|
227+
trait DOM_Element {
228+
fn[E : DOM_Element] querySelector(Self, String) -> E = _
229+
to_element(Self) -> Element
230+
}
231+
232+
///|
233+
extern "js" fn Element::querySelector(
234+
self : Element,
235+
selectors : String
236+
) -> Element = "(self,selector) => self.querySelector(selector)"
237+
238+
///|
239+
fn[A, B] coerce(x : A) -> B = "%identity"
240+
241+
///|
242+
impl DOM_Element with querySelector(self, selectors) {
243+
coerce(Element::querySelector(coerce(self), selectors))
244+
}
245+
246+
///|
247+
#external
248+
type HTMLCanvasElement
249+
250+
///|
251+
#external
252+
type HTMLDivElement
253+
254+
///|
255+
#external
256+
type HTMLAnchorElement
257+
258+
///|
259+
#external
260+
type HTMLImageElement
261+
262+
///|
263+
impl DOM_Element for HTMLCanvasElement with to_element(self) = "%identity"
264+
265+
///|
266+
impl DOM_Element for HTMLDivElement with to_element(self) = "%identity"
267+
268+
///|
269+
impl DOM_Element for HTMLAnchorElement with to_element(self) = "%identity"
270+
271+
///|
272+
impl DOM_Element for HTMLImageElement with to_element(self) = "%identity"
273+
274+
///|
275+
test {
276+
let x : HTMLDivElement = {
277+
...
278+
}
279+
let e : HTMLImageElement = x.querySelector("x")
280+
281+
}
282+
```
283+
284+
### Show trait
285+
286+
```moonbit
287+
pub(open) trait Show {
288+
fn[L : Logger] output(Self,L) -> Unit
289+
to_string(Self) -> String = _
290+
}
291+
```
292+
293+
## Possible alternatives
294+
295+
use [virtual packages](https://docs.moonbitlang.com/en/latest/language/packages.html#virtual-packages) to check API consistency,
296+
like OCaml [module signature](https://ocaml.org/manual/5.3/moduleexamples.html#s:signature), but module signature only can check module, can not check for type.

0 commit comments

Comments
 (0)