Skip to content
Snippets Groups Projects
Commit 92ed1cf8 authored by Julien Richard-Foy's avatar Julien Richard-Foy
Browse files

Add material for type-directed programming

parent e7c6e8e5
No related branches found
No related tags found
No related merge requests found
% Implicit Programming — Motivating Example
% Type-Directed Programming — Motivating Example
%
%
......@@ -114,7 +114,7 @@ object Ordering {
Reducing Boilerplate
====================
Problem: Passing around `Ordering` values is cumbersome.
Problem: Passing around `Ordering` arguments is cumbersome.
~~~
sort(xs)(Ordering.Int)
......@@ -122,6 +122,6 @@ sort(ys)(Ordering.Int)
sort(strings)(Ordering.String)
~~~
Sorting a `List[Int]` instance always uses the same `Ordering.Int` value,
sorting a `List[String]` instance always uses the same `Ordering.String`
value, and so on…
Sorting a `List[Int]` value always uses the same `Ordering.Int` argument,
sorting a `List[String]` value always uses the same `Ordering.String`
argument, and so on…
% Implicits
% Type-Directed Programming
%
%
......@@ -9,7 +9,7 @@ Reminder: General `sort` Operation
def sort[A](xs: List[A])(ord: Ordering[A]): List[A] = ...
~~~
Problem: Passing around `Ordering` values is cumbersome.
Problem: passing around `Ordering` arguments is cumbersome.
~~~
sort(xs)(Ordering.Int)
......@@ -17,23 +17,20 @@ sort(ys)(Ordering.Int)
sort(strings)(Ordering.String)
~~~
Sorting a `List[Int]` instance always uses the same `Ordering.Int` value,
sorting a `List[String]` instance always uses the same `Ordering.String`
value, and so on…
Sorting a `List[Int]` value always uses the same `Ordering.Int` argument,
sorting a `List[String]` value always uses the same `Ordering.String`
argument, and so on…
Implicit Parameters
===================
We can reduce the boilerplate by making `ord` an **implicit** parameter.
We can reduce the boilerplate by making `ord` an **implicit parameter**.
~~~
def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A] = ...
def sort[A](xs: List[A])(given ord: Ordering[A]): List[A] = ...
~~~
- A method can have only one implicit parameter list, and it must be the last
parameter list given.
Then calls to `sort` can omit the `ord` parameter:
Then, calls to `sort` can omit the `ord` parameter:
~~~
sort(xs)
......@@ -41,14 +38,13 @@ sort(ys)
sort(strings)
~~~
The compiler infers the implicit parameter value based on the
**queried type**.
The compiler infers the argument value based on its type.
Implicit Parameters (2)
=======================
~~~
def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A] = ...
def sort[A](xs: List[A])(given ord: Ordering[A]): List[A] = ...
val xs: List[Int] = ...
~~~
......@@ -67,51 +63,129 @@ sort[Int](xs)
->
In this case, the type of `ord` is `Ordering[Int]`.
->
~~~
sort[Int](xs)(given Ordering.Int)
~~~
(assuming there exists a **given instance** of type `Ordering[Int]` named `Ordering.Int`)
Given Clauses Syntax Reference (1)
==================================
There can be several `given` parameter clauses in a definition and `given`
parameter clauses can be freely mixed with normal ones.
~~~
def sort[A](xs: List[A])(given ord: Ordering[Int]): List[A] = ...
~~~
At call site, the arguments of the given clause are usually left out, although it is
possible to explicitly pass them:
~~~
// Argument inferred by the compiler
sort(xs)
// Explicit argument
sort(xs)(given Ordering.Int.reverse)
~~~
Given Clauses Syntax Reference (2)
==================================
Multiple parameters can be in a `given` clause:
~~~
def f(x: Int)(given foo: Foo, bar: Bar) = ...
~~~
Or, there can be several `given` clauses in a row:
~~~
def f(x: Int)(given foo: Foo)(given bar: Bar) = ...
~~~
Given Clauses Syntax Reference (3)
==================================
Parameters of a given clause can be anonymous:
~~~
sort[Int](xs)(Ordering.Int)
def sort[A](xs: List[A])(given Ordering[A]): List[A] = ...
~~~
In this case, the queried type is `Ordering[Int]`.
This is useful if the body of `sort` does not mention `ord`
at all, but simply relies on the fact that there is an
available given instance of type `Ordering[A]`.
Implicit Parameters Resolution
==============================
Say, a function takes an implicit parameter of type `T`.
The compiler will search an implicit **definition** that:
The compiler will search a **given instance** that:
- is marked `implicit`,
- has a type compatible with `T`,
- is visible at the point of the function call, or is defined
in a companion object associated with `T`.
in a companion object *associated* with `T`.
If there is a single (most specific) definition, it will be taken
If there is a single (most specific) instance, it will be taken
as actual arguments for the implicit parameter.
Otherwise it’s an error.
Implicit Definitions
====================
Given Instances
===============
For the previous example to work, the `Ordering.Int` value definition
must be marked `implicit`:
For the previous example to work, the `Ordering.Int` definition
must be a `given` instance:
~~~
object Ordering {
implicit val Int: Ordering[Int] = ...
given Int: Ordering[Int] {
def compare(x: Int, y: Int): Int = ...
}
}
~~~
Implicit Search
===============
This code defines a given instance of type `Ordering[Int]`, named `Int`.
Given Instances Syntax Reference
================================
Given instances can be anonymous. Just omit the instance name:
~~~
given Ordering[Int] { ... }
~~~
Given instances can take type parameters and implicit parameters:
~~~
given [A, B](given Ordering[A], Ordering[B]): Ordering[(A, B)] { ... }
~~~
An alias can be used to define a given instance:
~~~
given Ordering[Int] = ...
~~~
Given Instances Search Scope
============================
The implicit search for a type `T` includes:
The search for a given instance of type `T` includes:
- all the implicit definitions that are visible (inherited, imported,
or defined in an enclosing scope),
- the *implicit scope* of type `T`, made of implicit definitions found
- all the given instances that are visible (inherited, or defined in
an enclosing scope),
- all the given instances that are imported via a “given import”,
- the *implicit scope* of type `T`, made of given instances found
in a companion object *associated* with `T`. In essence$^*$, the types
associated with a type `T` are:
- if `T` is a compound type $T_1 with T_2 ... with T_n$, the union
......@@ -120,63 +194,66 @@ The implicit search for a type `T` includes:
of the parts of $S$ and $T_1$, ..., $T_n$,
- otherwise, just `T` itself.
->
In the case of the `sort(xs)` call, the compiler looks for an implicit
`Ordering[Int]` definition, which is found in the `Ordering` companion
object.
Implicit Not Found
==================
No Given Instance Found
=======================
If there is no available implicit definition matching the queried type,
If there is no available given instance matching the queried type,
an error is reported:
~~~
scala> def f(implicit n: Int) = ()
scala> def f(given n: Int) = ()
scala> f
^
error: could not find implicit value for parameter n: Int
error: no implicit argument of type Int was found for parameter n of method f
~~~
Ambiguous Implicit Definitions
==============================
Ambiguous Given Instances
=========================
If more than one implicit definition are eligible, an **ambiguity** is reported:
If more than one given instances are eligible, an **ambiguity** is reported:
~~~
scala> implicit val x: Int = 0
scala> implicit val y: Int = 1
scala> def f(implicit n: Int) = ()
scala> given x: Int = 0
scala> given y: Int = 1
scala> def f(given n: Int) = ()
scala> f
^
error: ambiguous implicit values:
both value x of type => Int
and value y of type => Int
match expected type Int
error: ambiguous implicit arguments:
both value x and value y
match type Int of parameter n of method f
~~~
Priorities
==========
Actually, several implicit definitions matching the same type don’t generate an
Actually, several given instances matching the same type don’t generate an
ambiguity if one is **more specific** than the other.
In essence$^{*}$, a definition `a: A` is more specific than a definition `b: B` if:
In essence$^{*}$, a `given a: A` definition is more specific than a
`given b: B` definition if:
- type `A` is a subtype of type `B`,
- type `A` has more “fixed” parts,
- `a` is defined in a class or object which is a subclass of the class defining `b`.
- `a` is defined in a class or object which is a subclass of the class defining `b`,
- `a` is in a closer lexical scope than `b`.
Priorities: Example (1)
=======================
Which implicit definition matches the queried `Int` implicit parameter when
Which given instance matches the `Int` implicit parameter when
the `f` method is called?
~~~
implicit def universal[A]: A = ???
implicit def int: Int = ???
given universal[A]: A = ???
given int: Int = ???
def f(implicit n: Int) = ()
def f(given n: Int) = ()
f
~~~
......@@ -184,17 +261,34 @@ f
Priorities: Example (2)
=======================
Which implicit definition matches the queried `Int` implicit parameter when
Which given instance matches the `Int` implicit parameter when
the `f` method is called?
~~~
trait A {
implicit val x: Int = 0
given x: Int = 0
}
trait B extends A {
implicit val y: Int = 1
given y: Int = 1
def f(given n: Int) = ()
f
}
~~~
Priorities: Example (3)
=======================
Which implied instance matches the queried `Int` implicit parameter when
the `f` method is called?
~~~
given x: Int = 0
locally {
given y: Int = 1
def f(implicit n: Int) = ()
def f(given n: Int) = ()
f
}
......@@ -203,7 +297,7 @@ trait B extends A {
Context Bounds
==============
A syntactic sugar allows the omission of the implicit parameter list:
A syntactic sugar allows the omission of the given clause:
~~~
def printSorted[A: Ordering](as: List[A]): Unit = {
......@@ -211,8 +305,8 @@ def printSorted[A: Ordering](as: List[A]): Unit = {
}
~~~
Type parameter `A` has one **context bound**: `Ordering`. There must be an
implicit value with type `Ordering[A]` at the point of application.
Type parameter `A` has one **context bound**: `Ordering`. There must be a
given instance of type `Ordering[A]` at the point of application.
More generally, a method definition such as:
......@@ -220,36 +314,35 @@ $def f[A: U_1 ... : U_n](ps): R = ...$
Is expanded to:
$def f[A](ps)(implicit ev_1: U_1[A], ..., ev_n: U_n[A]): R = ...$
$def f[A](ps)(given U_1[A], ..., U_n[A]): R = ...$
Implicit Query
==============
At any point in a program, one can **query** an implicit value of
a given type by calling the `implicitly` operation:
At any point in a program, one can **query** a given instance of
a specific type by calling the `summon` operation:
~~~
scala> implicitly[Ordering[Int]]
scala> summon[Ordering[Int]]
res0: Ordering[Int] = scala.math.Ordering$Int$@73564ab0
~~~
`implicitly` is not a special keyword, it is defined as a library operation:
`summon` is not a special keyword, it is defined as a library operation:
~~~
def implicitly[A](implicit value: A): A = value
def summon[A](given value: A): value.type = value
~~~
Summary
=======
In this lecture we have introduced the concept of **implicit programming**,
In this lecture we have introduced the concept of **type-directed programming**,
a language mechanism that infers **values** by using **type** information.
There has to be a **unique** implicit definition matching the queried type
There has to be a **unique** given instance matching the queried type
for it to be used by the compiler.
Implicit values are searched in the enclosing **lexical scope** (imports,
parameters, inherited members) as well as in the **implicit scope** made
of implicits defined in companion objects of types associated with the
Given instances are searched in the enclosing **lexical scope** (imports,
parameters, inherited members) as well as in the **given instances search scope**
made of given instances defined in companion objects of types associated with the
queried type.
% Type Classes vs Inheritance
% Type Classes vs Subtyping
%
%
......@@ -12,8 +12,8 @@ to achieve *ad hoc* polymorphism:
trait Ordering[A] { def lt(a1: A, a2: A): Boolean }
object Ordering {
implicit val Int: Ordering[Int] = (x, y) => x < y
implicit val String: Ordering[String] = (s, t) => (s compareTo t) < 0
given Int: Ordering[Int] = (x, y) => x < y
given String: Ordering[String] = (s, t) => (s compareTo t) < 0
}
def sort[A: Ordering](xs: List[A]): List[A] = ...
......@@ -38,14 +38,19 @@ def sort2(xs: List[Ordered]): List[Ordered] = {
}
~~~
How do these approaches compare?
Both `sort` and `sort2` can sort lists containing elements of an arbitrary
type `A` as long as there is an implicit instance of `Ordering[A]`, or `A`
is a subtype of `Ordered`, respectively.
Usage
=====
How does `sort2` compare to `sort`?
Return Type is Less Precise
===========================
(Assuming that `Int <: Ordered`)
~~~
val ints: List[Int] = List(1, 3, 2)
val sortedInts: List[Int] = sort2(ints)
~~~
......@@ -64,8 +69,8 @@ error: type mismatch;
def sort2(xs: List[Ordered]): List[Ordered]
~~~
First Improvement
=================
First Improvement: Introduce Type Parameter `A`
===============================================
~~~
def sort2[A <: Ordered](xs: List[A]): List[A] = // ... same implementation
......@@ -97,12 +102,31 @@ type with new operations without changing the original definition of the data ty
Implementing an Operation for a Custom Data Type
================================================
Here is how we can add the comparison operations to the `Rational` type.
~~~
case class Rational(num: Int, denom: Int)
~~~
->
~~~
object Rational {
given Ordering[Rational] =
(x, y) => x.num * y.denom < y.num * x.denom
}
sort(List(Rational(1, 2), Rational(1, 3)))
~~~
(The `Ordering[Rational]` given definition could be in a different project)
Alternatively, Using Subtyping Polymorphism
===========================================
Let’s see what happens with the subtyping approach, if we make
`Rational` extend `Ordered`:
~~~
case class Rational(num: Int, denom: Int) extends Ordered {
def lt(other: Ordered) = other match {
......@@ -135,39 +159,67 @@ def sort2[A <: Ordered[A]](xs: List[A]): List[A] = {
}
~~~
Implementing an Operation for a Custom Data Type (2)
====================================================
Subtyping vs Type Classes
=========================
~~~
case class Rational(num: Int, denom: Int)
Subtyping alone is less practical than type classes to achieve polymorphism.
object Rational {
implicit val ordering: Ordering[Rational] =
(x, y) => x.num * y.denom < y.num * x.denom
}
Often, you also need to introduce bounded type parameters, or even f-bounded
type parameters.
When Are Operation Implementations Resolved?
============================================
Another difference between subtyping polymorphism and type classes is **when**
the implementation of an operation is resolved.
Type Class Operations Resolution
================================
Remind the `sort` definition:
sort(Rational(1, 2), Rational(1, 3))
~~~
def sort[A](xs: List[A])(given ord: Ordering[A]): List[A] = {
...
... if ord.lt(x, y) then ...
...
}
~~~
->
The implicit `Ordering[A]` parameter is resolved by the compiler at **compilation
time**
(The `Ordering[Rational]` instance definition could be in a different project)
Subtype Operations Resolution
=============================
Dispatch Time
=============
Remind the (simplest) `sort2` definition:
Another difference between subtyping polymorphism and type classes is **when**
the dispatch happens.
~~~
def sort2(xs: List[Ordered]): List[Ordered] = {
...
... if x lt y then ...
...
}
~~~
The implementation of the `lt` operation is resolved at **run time** by selecting
the implementation of the actual type of `x`
- With type classes, the implicit instance is resolved at **compilation time**,
- With subtyping polymorphism, the actual implementation of a method is resolved
at **run time**.
When Are Operation Implementations Resolved?
============================================
- With type classes, the implicit parameter is resolved at **compilation time**,
- With subtyping, the actual implementation of a method is resolved at **run time**.
Summary
=======
In this lecture we have seen that type classes support retroactive
extensibility.
In this lecture we have seen that type classes make it possible to decouple
data type definitions from the operations they support. In particular, it
is possible to retroactively add new operations to a data type.
Conversely, it is possible to write polymorphic programs that work with any
data type supporting these operations.
The dispatch happens at compile time with type classes, whereas it
happens at run time with subtype polymorphism.
The operation implementations are resolved at compile-time with type classes,
whereas they are resolved at run-time with subtyping.
% Higher-Order Implicits
% Higher-Order Givens
%
%
Higher-Order Implicits (1)
==========================
Higher-Order Given Instances (1)
================================
Consider how we order two `String` values:
......@@ -17,25 +17,23 @@ Consider how we order two `String` values:
element type `A` for which there is an implicit `Ordering[A]`
instance?
Higher-Order Implicits (2)
==========================
Higher-Order Given Instances (2)
================================
~~~
implicit def listOrdering[A](implicit
ord: Ordering[A]
): Ordering[List[A]] = ...
given listOrdering[A](given Ordering[A]): Ordering[List[A]] = ...
~~~
Higher-Order Implicits (3)
==========================
Higher-Order Given Instances (3)
================================
~~~
implicit def listOrdering[A](implicit
ord: Ordering[A]
): Ordering[List[A]] = { (xs0, ys0) =>
given listOrdering[A]
(given ord: Ordering[A]): Ordering[List[A]] = { (xs0, ys0) =>
def loop(xs: List[A], ys: List[A]): Boolean = (xs, ys) match {
case (x :: xsTail, y :: ysTail) => ord.lt(x, y) && loop(xsTail, ysTail)
case (xs, ys) => ys.nonEmpty
case (xs, ys) => xs.isEmpty && ys.nonEmpty
}
loop(xs0, ys0)
}
......@@ -46,12 +44,12 @@ scala> sort(List(List(1, 2, 3), List(1), List(1, 1, 3)))
res0: List[List[Int]] = List(List(1), List(1, 1, 3), List(1, 2, 3))
~~~
Higher-Order Implicits (4)
==========================
Higher-Order Given Instances (4)
================================
~~~
def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A]
implicit def listOrdering[A](implicit ord: Ordering[A]): Ordering[List[A]]
def sort[A](xs: List[A])(given Ordering[A]): List[A]
given listOrdering[A](given Ordering[A]): Ordering[List[A]]
val xss: List[List[Int]] = ...
sort(xss)
......@@ -66,46 +64,48 @@ sort[List[Int]](xss)
->
~~~
sort[List[Int]](xss)(listOrdering)
sort[List[Int]](xss)(given listOrdering)
~~~
->
~~~
sort[List[Int]](xss)(listOrdering(Ordering.Int))
sort[List[Int]](xss)(given listOrdering(given Ordering.Int))
~~~
Higher-Order Implicits (5)
==========================
Higher-Order Given Instances (5)
================================
An arbitrary number of implicit definitions can be combined
until the search hits a “terminal” definition:
An arbitrary number of given instances can be combined
until the search hits a “terminal” instance:
~~~
implicit def a: A = ...
implicit def aToB(implicit a: A): B = ...
implicit def bToC(implicit b: B): C = ...
implicit def cToD(implicit c: C): D = ...
given a: A = ...
given aToB(given A): B = ...
given bToC(given B): C = ...
given cToD(given C): D = ...
implicitly[D]
summon[D]
~~~
Recursive Implicits
===================
Recursive Given Instances
=========================
~~~
trait A
implicit def loop(implicit a: A): A = a
given loop(given a: A): A = a
implicitly[A]
summon[A]
~~~
->
~~~
^
error: diverging implicit expansion for type A
starting with method loop
^
error: no implicit argument of type A was found for parameter x of method the.
I found:
loop(/* missing */summon[A])
But method loop produces a diverging implicit search when trying to match type A.
~~~
Summary
......@@ -113,6 +113,6 @@ Summary
In this lecture, we have seen:
- implicit definitions can also take implicit parameters
- an arbitrary number of implicitly definitions can be chained
until a terminal definition is reached
- given instance definitions can also have given clauses
- an arbitrary number of given instances can be chained
until a terminal instance is reached
% Implicit Conversions
% Extension Methods
%
%
Implicit Conversions
====================
The last implicit-related mechanism of the language is implicit
**conversions**.
They make it possible to convert an expression to a different
type.
This mechanism is usually used to provide more ergonomic APIs:
~~~
// { "name": "Paul", "age": 42 }
Json.obj("name" -> "Paul", "age" -> 42)
val delay = 15.seconds
~~~
Type Coercion: Motivation (1)
Extension Methods: Motivation
=============================
~~~
sealed trait Json
case class JNumber(value: BigDecimal) extends Json
case class JString(value: String) extends Json
case class JBoolean(value: Boolean) extends Json
case class JArray(elems: List[Json]) extends Json
case class JObject(fields: (String, Json)*) extends Json
~~~
In the previous lectures, we have seen that type classes could be
used to retroactively add operations to existing data types.
->
However, when the operations are defined outside of the data types,
they can’t be called like methods on these data type instances.
~~~
// { "name": "Paul", "age": 42 }
JObject("name" -> JString("Paul"), "age" -> JNumber(42))
~~~
Problem: Constructing JSON objects is too verbose.
Type Coercion: Motivation (2)
=============================
case class Rational(num: Int, denom: Int)
given Ordering[Rational] = ...
val x: Rational = ...
val y: Rational = ...
~~~
sealed trait Json
case class JNumber(value: BigDecimal) extends Json
case class JString(value: String) extends Json
case class JBoolean(value: Boolean) extends Json
case class JArray(elems: List[Json]) extends Json
case class JObject(fields: (String, Json)*) extends Json
~~~
~~~
// { "name": "Paul", "age": 42 }
Json.obj("name" -> "Paul", "age" -> 42)
x < y
// ^
// value '<' is not a member of 'Rational'
~~~
How could we support the above user-facing syntax?
Type Coercion: Motivation (3)
=============================
~~~
// { "name": "Paul", "age": 42 }
Json.obj("name" -> "Paul", "age" -> 42)
~~~
Extension Methods (1)
=====================
What could be the type signature of the `obj` constructor?
**Extension methods** make it possible to add methods to a type after the
type is defined.
->
~~~
def obj(fields: (String, Any)*): Json
~~~
->
def (lhs: Rational) < (rhs: Rational): Boolean =
lhs.num * rhs.denom < rhs.num * lhs.denom
Allows invalid JSON objects to be constructed!
val x: Rational = ...
val y: Rational = ...
x < y // It works!
~~~
Json.obj("name" -> ((x: Int) => x + 1))
~~~
We want invalid code to be signaled to the programmer with a
compilation error.
Type Coercion (1)
=================
Extension Methods (2)
=====================
~~~
object Json {
def obj(fields: (String, JsonField)*): Json =
JObject(fields.map(_.toJson): _*)
trait JsonField {
def toJson: Json
}
}
def (lhs: Rational) < (rhs: Rational): Boolean = ...
~~~
Type Coercion (2)
=================
- An extension method definition is a method definition with a parameter clause
**before** the method name,
- The leading parameter clause must have exactly one parameter,
- Extension methods are applicable if they are visible (by being defined, inherited,
or imported) in a scope enclosing the point of the application.
~~~
trait JsonField {
def toJson: Json
}
Given Extension Methods (1)
===========================
object JsonField {
implicit def stringToJsonField(s: String): JsonField = () => JString(s)
implicit def intToJsonField(n: Int): JsonField = () => JNumber(n)
...
implicit def jsonToJsonField(j: Json): JsonField = () => j
}
~~~
How can we add the `<` operation to any type `A` for which there is a given
`Ordering[A]` instance?
Type Coercion: Usage
====================
->
~~~
Json.obj("name" -> "Paul", "age" -> 42)
def (lhs: A) < [A](rhs: A)(given ord: Ordering[A]): Boolean =
ord.lessThan(lhs, rhs)
~~~
->
The compiler implicitly inserts the following conversions:
At application site, the compiler will resolve the implicit `ord` parameter
according to the operands type.
~~~
Json.obj(
"name" -> Json.JsonField.stringToJsonField("Paul"),
"age" -> Json.JsonField.intToJsonField(42)
)
~~~
Given Extension Methods (2)
===========================
Extension Methods: Motivation (1)
=================================
Alternatively, the `<` extension method could be directly defined in the
`Ordering` type class:
~~~
case class Duration(value: Int, unit: TimeUnit)
trait Ordering[A] {
def (lhs: A) < (rhs: A): Boolean
}
~~~
->
~~~
val delay = Duration(15, TimeUnit.Second)
~~~
Extension Methods: Motivation (2)
=================================
Such extension methods are applicable if there is a given instance
at the point of the application.
~~~
case class Duration(value: Int, unit: TimeUnit)
~~~
Complete Example (1)
====================
~~~
val delay = 15.seconds
trait Ordering[A] {
def (lhs: A) < (rhs: A): Boolean
}
~~~
How could we support the above user-facing syntax?
Extension Methods
=================
Complete Example (2)
====================
~~~
case class Duration(value: Int, unit: TimeUnit)
object Duration {
object Syntax {
implicit def hasSeconds(n: Int): HasSeconds = new HasSeconds(n)
object Ordering {
given Ordering[Int] {
def (lhs: Int) < (rhs: Int) = lhs < rhs
}
class HasSeconds(n: Int) {
def seconds: Duration = Duration(n, TimeUnit.Second)
given [A](given Ordering[A]): Ordering[List[A]] {
def (lhs: List[A]) < (rhs: List[A]) = {
def loop(xs: List[A], ys: List[A]): Boolean = (xs, ys) match {
case (x :: xsT, y :: ysT) => (x < y) && loop(xsT, ysT)
case (xs, ys) => xs.isEmpty && ys.nonEmpty
}
loop(lhs, rhs)
}
}
}
~~~
Extension Methods: Usage
========================
~~~
import Duration.Syntax._
Complete Example (3)
====================
val delay = 15.seconds
~~~
case class Rational(num: Int, denom: Int)
->
The compiler implicitly inserts the following conversion:
~~~
val delay = hasSeconds(15).seconds
object Rational {
given Ordering[Rational] {
def (lhs: Rational) < (rhs: Rational) =
lhs.num * rhs.denom < rhs.num * lhs.denom
}
}
~~~
Implicit Conversions
Complete Example (4)
====================
The compiler looks for implicit conversions on an expression `e` of type `T`
in the following situations:
~~~
import Ordering.given
- `T` does not conform to the expression’s expected type,
- in a selection `e.m`, if member `m` is not accessible on `T`,
- in a selection `e.m(args)`, if member `m` is accessible on `T` but is not
applicable to the arguments `args`.
val i = 1
val j = 2
val p = Rational(1, 2)
val q = Rational(1, 3)
val xs = List(1, 2, 3)
val ys = List(1, 2, 4)
Note: at most one implicit conversion can be applied to a given expression.
i < j // true
p < q // false
xs < ys // true
~~~
Summary
=======
- Implicit conversions can improve the ergonomics of an API
In this lecture, we have seen:
- how to define extension methods outside of a type definition,
- how to define extension methods for type class instances.
% Implicit Conversions
%
%
Implicit Conversions
====================
**Implicit conversions** make it possible to convert an expression to a different
type.
This mechanism is usually used to provide more ergonomic APIs.
->
Example: API for defining JSON documents.
~~~
// { "name": "Paul", "age": 42 }
Json.obj("name" -> "Paul", "age" -> 42)
~~~
Type Coercion: Motivation (1)
=============================
~~~
sealed trait Json
case class JNumber(value: BigDecimal) extends Json
case class JString(value: String) extends Json
case class JBoolean(value: Boolean) extends Json
case class JArray(elems: List[Json]) extends Json
case class JObject(fields: (String, Json)*) extends Json
~~~
->
~~~
// { "name": "Paul", "age": 42 }
JObject("name" -> JString("Paul"), "age" -> JNumber(42))
~~~
Problem: Constructing JSON objects is too verbose.
Type Coercion: Motivation (2)
=============================
~~~
sealed trait Json
case class JNumber(value: BigDecimal) extends Json
case class JString(value: String) extends Json
case class JBoolean(value: Boolean) extends Json
case class JArray(elems: List[Json]) extends Json
case class JObject(fields: (String, Json)*) extends Json
~~~
~~~
// { "name": "Paul", "age": 42 }
Json.obj("name" -> "Paul", "age" -> 42)
~~~
How could we support the above user-facing syntax?
Type Coercion: Motivation (3)
=============================
~~~
// { "name": "Paul", "age": 42 }
Json.obj("name" -> "Paul", "age" -> 42)
~~~
What could be the type signature of the `obj` constructor?
->
~~~
def obj(fields: (String, Any)*): Json
~~~
->
Allows invalid JSON objects to be constructed!
~~~
Json.obj("name" -> ((x: Int) => x + 1))
~~~
We want invalid code to be signaled to the programmer with a
compilation error.
Type Coercion (1)
=================
~~~
object Json {
def obj(fields: (String, JsonField)*): Json =
JObject(fields.map(_.toJson): _*)
trait JsonField {
def toJson: Json
}
}
~~~
Type Coercion (2)
=================
~~~
trait JsonField {
def toJson(): Json
}
object JsonField {
given stringToJsonField: Conversion[String, JsonField] =
(s: String) => () => JString(s)
given intToJsonField: Conversion[Int, JsonField] =
(n: Int) => () => JNumber(n)
...
given jsonToJsonField: Conversion[JSon, JsonField] =
(j: Json) => () => j
}
~~~
Type Coercion: Usage
====================
~~~
Json.obj("name" -> "Paul", "age" -> 42)
~~~
->
The compiler implicitly inserts the following conversions:
~~~
Json.obj(
"name" -> Json.JsonField.stringToJsonField.apply("Paul"),
"age" -> Json.JsonField.intToJsonField.apply(42)
)
~~~
Implicit Conversions
====================
The compiler looks for implicit conversions on an expression `e` of type `T`
if `T` does not conform to the expression’s expected type `S`.
In such a case, the compiler looks in the implicit scope for a given instance of
type `Conversion[T, S]`.
Note: at most one implicit conversion can be applied to a given expression.
Summary
=======
- Implicit conversions can improve the ergonomics of an API
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment