GuideThe Silo HandbookTraitssource

Source guide/traits.simd

1# Traits
2
3:gloss[Traits](./A1-glossary.simd#trait) are Silo's mechanism for
4polymorphism. You saw them mentioned in every chapter so far —
5arithmetic operators, comparison, hashing, formatting, and
6iteration are all trait-driven — and this chapter makes the
7whole picture explicit.
8
9If you know Rust: traits here work almost exactly like Rust
10traits, down to the vocabulary. The main differences are cosmetic
11(different keyword) and one design choice Silo took in the
12opposite direction (no orphan rule). Haskell users: these are
13typeclasses with a little less ceremony.
14
15## Declaring a trait
16
17A `:trait` declares a named interface with one or more methods:
18
19```silo
20:trait Printable self
21  .display ( self -> Str )
22:end
23```
24
25The `self` after the trait name is the primary type parameter —
26the type that the trait is "about". Each method's signature uses
27`self` the same way Rust's `Self` works. `Printable` has one
28method; larger traits have more.
29
30## Implementing a trait
31
32`:impl` provides an implementation of a trait for a specific type:
33
34```silo
35:impl Printable Point
36  .display ( Point -> Str )
37    pop-> p
38    p .x p .y "({}, {})" format ;
39:end
40```
41
42Read `:impl Printable Point` as "this is the `Printable` impl
43for `Point`." The trait name and the types flow left-to-right,
44the same way a stack effect or a type application does. Inside
45the `:impl` block, each method's body ends with a semicolon;
46the body is ordinary Silo code. Parentheses are only needed when
47a nested type application would otherwise be ambiguous, e.g.
48`:impl Display (Vec Int)`.
49
50Once the impl is in scope, anywhere `(Printable something)` is
51expected, a `Point` satisfies the constraint, and `.display` on a
52`Point` dispatches to this body.
53
54## Default method bodies
55
56A trait method can carry a default body. If an `:impl` doesn't
57override the method, the default body is used:
58
59```silo
60:trait Greetable self
61  .greeting ( self -> Str )
62    .display "Hello, " swap + ;  # default body, ends with ;
63:end
64```
65
66A default body lets a trait offer derived methods on top of a
67smaller required surface. `Greetable` above depends on
68`Printable` being available (it calls `.display`), so you'd
69usually pair this with a constraint `{ (Printable self) }` in the
70trait header to make that dependency explicit.
71
72## Primary parameters vs associated types
73
74Not every trait is "about" a single type. Arithmetic is the
75obvious case: `+` takes two operands (which might be different
76types) and produces a third.
77
78Silo splits a trait's type-level inputs into two groups:
79**primary parameters** (the ones the caller picks, written
80before a `|` wall) and **associated types** (the ones the impl
81determines, written after the `|`). `Add` is a worked example:
82
83```silo
84:trait Add rhs | result
85  .+ ( Self rhs -> result )
86:end
87```
88
89`Self` and `rhs` are the primary parameters: callers fix them
90by choosing which two values to add. `result` is an associated
91type — the impl determines it from the primary types. For
92`Int + F64`, the only impl that matches returns `F64`; the
93caller doesn't have to annotate anything.
94
95An impl lists the primary types first and the associated type
96after the `|`:
97
98```silo
99:impl Add Int | Int
100  .+ ( Int Int -> Int ) ... ;
101:end
102
103:impl Add F64 | F64
104  .+ ( F64 F64 -> F64 ) ... ;
105:end
106
107:impl Add F64 | F64
108  .+ ( Int F64 -> F64 ) ... ;
109:end
110```
111
112Read `:impl Add Int | Int` as "`Add` for `Self = Int`,
113`rhs = Int`, with the associated type `result = Int`." `Self`
114is always the first position; the remaining primary parameters
115follow; `|` separates the associated types.
116
117These are Silo's equivalent of Rust's associated types
118(`type Output = …` in a trait impl). They live at the impl
119level rather than travelling with every call, which is why
120`a b +` doesn't need the caller to name a third type.
121
122## Qualified dispatch
123
124Most of the time, method resolution is automatic: `my-val
125.display` finds the impl that matches `my-val`'s type. When you
126need to disambiguate — for instance, `.default` is nullary and
127has no stack value to dispatch on — use `(Type .method)`:
128
129```silo
130(Point .default)               # ⌊Point{ x ↦ 0.0  y ↦ 0.0 }⌉
131(Vec .default)                 # empty Vec
13242 (F64 .from)                 # calls (From F64 Int).from
133```
134
135`(Type .method)` is "call `method` from the impl for this
136`Type`." It's the Silo equivalent of Rust's
137`<Type as Trait>::method`, but you don't name the trait —
138Silo infers it from the method name and the impl context.
139
140## `From` / `Into` / `TryFrom` / `TryInto`
141
142Conversion between types is standardised through four traits.
143`From` and `Into` are infallible; `TryFrom` and `TryInto` return
144`Result`:
145
146```silo
147:trait From target source
148  .from ( source -> target )
149:end
150
151:impl From F64 I64
152  .from ( I64 -> F64 ) int>float ;
153:end
154
15542 (F64 .from)                 # ⌊42.0⌉
156```
157
158Every `From` impl automatically supplies the reverse `Into`, so
159you rarely write `Into` impls by hand:
160
161```silo
162:fn as-float ( I64 -> F64 ) .into :end
163```
164
165The `.into` method resolves based on the return type — the
166context expects `F64`, so Silo picks the `From F64 I64` impl and
167calls its reverse.
168
169`TryFrom` and `TryInto` work the same way but return
170`(Result target err)`:
171
172```silo
17342 (U8 .try-from)              # ⌊Ok(42:U8)⌉
174300 (U8 .try-from)             # ⌊Err("value exceeds U8 range")⌉
175```
176
177## No orphan rule
178
179Silo deliberately has **no orphan rule**. Any package can
180implement any trait on any type, regardless of where the trait
181or the type is defined. Coherence is preserved by **scoped
182impls**: impls defined in the same package as the trait or the
183type are in scope automatically when you import them; impls
184defined elsewhere (*orphan impls*) require an explicit `:impl`
185line in your `:use` block to bring them into scope.
186
187This is the same philosophy OCaml's modular-implicits design took
188— rather than globally preventing third-party impls, let anyone
189write them, and rely on the importer to say which ones they
190want.
191
192```silo
193:use
194  :open geometry Point
195  :impl logging (Display Point)       # import an orphan impl explicitly
196:end
197```
198
199If two impls that both match could be in scope, the compiler
200applies **:gloss[specialisation](./A1-glossary.simd#specialisation)**:
201the more specific impl wins (e.g. `(From F64 Int)` beats a
202blanket `(From t v) { (v Default) }`). If no specific impl is
203more specific than another, it's a compile error and the
204importer has to disambiguate.
205
206See [the modules chapter](./16-modules.simd) for the full import
207story.
208
209## Operators are trait methods
210
211Every operator in Silo is just a name for a trait method:
212
213| Operator | Method   | Trait       |
214|----------|----------|-------------|
215| `+`      | `.+`     | `Add`       |
216| `-`      | `.-`     | `Sub`       |
217| `*`      | `.*`     | `Mul`       |
218| `/`      | `./`     | `Div`       |
219| `=`      | `.=`     | `Eq`        |
220| `≠`      | `.≠`     | `Eq`        |
221| `<`      | `.<`     | `Ord`       |
222| `>`      | `.>`     | `Ord`       |
223| `≤`      | `.≤`     | `Ord`       |
224| `≥`      | `.≥`     | `Ord`       |
225| `!`      | `.!`     | `Not`       |
226
227Writing `a b +` is exactly the same as writing `a b .+`. The
228operator notation is there because infix-ish expressions read
229better for numerics; nothing about it is magic, and you can
230define `+`-taking impls for your own types by implementing
231`Add`.
232
233## The core trait tour
234
235A short orientation to the most-used traits in the standard
236library. Each has its own chapter or section elsewhere; this is
237the at-a-glance view.
238
239| Trait                | Methods                                  | Purpose                                  |
240|----------------------|------------------------------------------|------------------------------------------|
241| `Eq`                 | `.=`, `.≠`                               | Equality                                 |
242| `Ord`                | `.<`, `.>`, `.≤`, `.≥`                   | Total ordering                           |
243| `Hash`               | `.hash`                                  | Hashable values                          |
244| `Default`            | `.default`                               | A canonical "zero" value                 |
245| `Display`, `Debug`   | `.fmt`, `.fmt-debug`                     | Formatting                               |
246| `Add`/`Sub`/`Mul`/`Div` | `.+`/`.-`/`.*`/`./`                   | Arithmetic                               |
247| `Not`                | `.!`                                     | Logical / bitwise negation               |
248| `From`/`Into`        | `.from`, `.into`                         | Infallible conversion                    |
249| `TryFrom`/`TryInto`  | `.try-from`, `.try-into`                 | Fallible conversion                      |
250| `Foldable`           | `.fold`, `.for-each`, `.any`, `.all`, `.count` | Eager traversal                    |
251| `Mappable`           | `.map`, `.flat-map`                      | Shape-preserving transform               |
252| `Filterable`         | `.filter`                                | Keep some elements                       |
253| `Indexed`            | `.get`, `.len`                           | Keyed lookup                             |
254| `Iterator`           | `.next`                                  | Lazy element-at-a-time consumption       |
255
256Most of these have `derive` shortcuts: putting `[Eq Hash Ord]`
257on a type declaration (see
258[chapter 5](./05-data.simd#derive-lists)) generates the obvious
259field-by-field impls automatically.
260
261## Static vs dynamic dispatch
262
263When the concrete type on the stack is known at compile time,
264method dispatch is **static**: the compiler emits a direct call
265to the matched impl's word, no indirection, no overhead.
266
267When the type is an :gloss[existential](./A1-glossary.simd#existential-type)
268like `Display` — "some value that implements `Display`, concrete
269type unknown" — dispatch is **dynamic**: the value is represented
270as a pair of (data handle, vtable pointer), and method calls go
271through the vtable.
272
273The compiler picks between the two automatically. You don't
274annotate dispatch strategy; the decision falls out of what the
275type system can see:
276
277```silo
278# Static dispatch — compiler knows the type:
27942 .display
280
281# Dynamic dispatch — any Display works at runtime:
282:fn log-value ( Display -> ) +Console
283  "{}" format print
284:end
285
28642 log-value                   # Int coerced to Display
287"hi" log-value                 # Str coerced to Display
288```
289
290The dynamic path costs one indirect call per method invocation —
291the same as a C++ virtual method or a Java interface call.
292Chapter 8's section on
293[downcasting existentials](./08-pattern-matching.simd#downcasting-existential-types)
294builds on this machinery.
295
296## Key points
297
298- `:trait` declares; `:impl (Trait Type)` provides the
299  implementation. Trait methods end with a semicolon when they
300  carry a body.
301- Traits can take multiple type parameters. Arithmetic operators
302  use three: `Add a b result`.
303- `(Type .method)` is qualified dispatch, for methods with no
304  disambiguating stack value (typically nullary methods like
305  `.default`).
306- `From`/`Into` and `TryFrom`/`TryInto` standardise conversion.
307  `From` auto-derives `Into`.
308- No orphan rule. Coherence via scoped impls — explicit `:impl`
309  imports for orphan impls. :gloss[Specialisation](./A1-glossary.simd#specialisation)
310  breaks ties.
311- Operators are trait-method sugar. `a b +` is `a b .+`.
312- Static dispatch when the type is known; dynamic dispatch via
313  vtable when the type is an existential. The compiler decides.
314
315Next: [effects, totality, and divergence](./10-effects.simd) —
316the system that tracks what side effects a word can have.