Newtypes, and giving one the traits of the type it wraps

A newtype beside aliases in "Your own types", and #:via and type/derive
for one in the traits chapter.

🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-5-5)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-21 08:25:02 +02:00
parent 6d9c9bf8a0
commit bd71102c19

View file

@ -354,6 +354,25 @@ A type alias is a different name for a type that already exists. It is declared
(type (: Stringalias string)) (type (: Stringalias string))
} }
@subsubsection{Newtypes}
A newtype is the opposite of an alias: a type of its own, which the compiler will not mix up with the type it wraps, but which is that type when the program runs, so it costs nothing.
@codeblock[#:lang "bjolang"]|{
(type (: UserId (Newtype int)))
(type (: OrderId (Newtype int)))
(def u (UserId 7)) ;; built by its name
(def (UserId n) u) ;; taken apart by a pattern: n is 7
(: (-> UserId string))
(defun (lookup-user id) ...)
(lookup-user (OrderId 7)) ;; a compile error: an OrderId is not a UserId
}|
A newtype does not get the traits of the type it wraps: @code{(+ u u)} is an error, though @code{(+ 7 7)} is not. It gets those you give it, as the section on traits shows.
@subsubsection{Records and structs} @subsubsection{Records and structs}
Records and structs are direct mappings to c# records and immutable structs. The constructor is the type name, together with parenthesised fields. As follows. Records and structs are direct mappings to c# records and immutable structs. The constructor is the type name, together with parenthesised fields. As follows.
@ -2172,6 +2191,31 @@ A trait may say that every type implementing it implements another trait too, wi
The prelude's @code{Ord} requires @code{Eq} this way, so a function with @code{(where (Ord %a))} can use @code{=} as well as @code{compare}. The prelude's @code{Ord} requires @code{Eq} this way, so a function with @code{(where (Ord %a))} can use @code{=} as well as @code{compare}.
@subsubsection{Giving a newtype the traits of the type it wraps}
A newtype is the type it wraps when the program runs, so it can borrow that type's implementation of a trait as it is, with @code{#:via}:
@codeblock[#:lang "bjolang"]|{
(type (: Meters (Newtype double)))
(impl (Eq Meters) #:via double)
(impl (Ord Meters) #:via double)
(impl (Num Meters) #:via double) ;; + - * /
(impl (Ordered Meters) #:via double) ;; < > <= >=
(+ (Meters 1.5) (Meters 2.0)) ;; (Meters 3.5)
(+ (Meters 1.5) 2) ;; a number literal is a Meters too
}|
@code{type/derive} writes these for you, for any trait, when the type is a newtype:
@codeblock[#:lang "bjolang"]|{
(type/derive (Eq Ord Num Ordered)
(: Meters (Newtype double)))
}|
A trait that should behave differently is written by hand instead. That is the other use of a newtype: the same data with a different behaviour, such as an @code{Eq} that ignores case on a @code{(Newtype string)}. Sets and maps of the newtype use that implementation, not the wrapped type's.
@subsubsection{Eq, Ord and ->str} @subsubsection{Eq, Ord and ->str}
Three traits from the prelude are worth knowing by heart, since the rest of the library leans on them. Three traits from the prelude are worth knowing by heart, since the rest of the library leans on them.