Skip to content

Commit 4073888

Browse files
committed
Discuss an alternative that makes conditional givens more like functions
1 parent d770fb8 commit 4073888

File tree

1 file changed

+165
-3
lines changed

1 file changed

+165
-3
lines changed

content/typeclasses-syntax.md

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ The possible variations are presented in the following.
499499
ListOrd[A]()
500500

501501
// Concrete class instance
502-
given Context
502+
given Context()
503503

504504
// Abstract or deferred given
505505
given Context = deferred
@@ -625,7 +625,7 @@ As an alternative, here is a version of new style given, but using the current `
625625
ListOrd[A]()
626626

627627
// Concrete class instance
628-
given context: Context // this would be a change of meaning from abstract given
628+
given context: Context()
629629

630630
// Abstract or deferred given
631631
given context: Context = deferred
@@ -637,9 +637,171 @@ As an alternative, here is a version of new style given, but using the current `
637637

638638
- It's more conventional than with `as`.
639639
- The double `:` in the first two examples is a bit jarring.
640-
- There would be a migration headache for concrete class instances, which currently mean abstract givens.
640+
- The concrete class instance needs explicit parentheses `()` to distinguish it
641+
from an abstract given. Once abstract givens are deprecated the compiler can give a hint that `()` is missing.
641642
- Overall, I find this version a bit more cumbersome to the one with `as`, but i could live with it (since I don't usually recommend to write named givens anyway).
642643

644+
### Alternative: Reinforce Similarity with Function Types
645+
646+
A reservation against the new syntax that is sometimes brought up is that the `=>` feels strange. I personally find the `=>` quite natural since it means implication, which is exactly what we want to express when we write a conditional given. This also corresponds to the meaning of arrow in functions since by the Curry-Howard isomorphism function types correspond to implications in logic.
647+
Besides `=>` is also used in other languages that support type classes (e.g.: Haskell).
648+
649+
As an example, the most natural reading of
650+
```scala
651+
given [A: Ord] => List[Ord[A]]
652+
```
653+
is _if `A` is `Ord` then `List[A]` is `Ord`_, or, equivalently, `A` is `Ord` _implies_ `List[A]` is `Ord`, hence the `=>`. Another way to see this is that
654+
the given clause establishes a _context function_ of type `[A: Ord] ?=> List[Ord[A]]` that is automatically applied to evidence arguments of type `Ord[A]` and that yields instances of type `List[Ord[A]]`. Since givens are in any case applied automatically to all their arguments, we don't need to specify that separately with `?=>`, a simple `=>` arrow is sufficiently clear and is easier to read.
655+
656+
Once one has internalized the analogy with implications and functions, one
657+
could argue the opposite, namely that the `=>` in a given clause is not sufficiently function-like. For instance, `given [A] => F[A]` looks like it implements a function type, but `given[A](using B[A]) => F[A]` looks like a mixture between a function type and a method signature.
658+
659+
A more radical and in some sense cleaner alternative is to decree that a given should always look like it implements a type. Conditional givens should look
660+
like they implement function types. Examples:
661+
```scala
662+
// Typeclass with context bound, as before
663+
given [A: Ord] => Ord[List[A]]:
664+
def compare(x: List[A], y: List[A]) = ...
665+
666+
// Typeclass with context parameter, instead of using clause
667+
given [A] => Ord[A] => Ord[List[A]]:
668+
def compare(x: List[A], y: List[A]) = ...
669+
670+
// Alias with context bound, as before
671+
given [A: Ord] => Ord[List[A]] =
672+
ListOrd[A]
673+
674+
// Alias with with context parameter, instead of using clause
675+
given [A] => Ord[A] => Ord[List[A]] =
676+
ListOrd[A]()
677+
```
678+
For completeness I also show two cases where the given clause uses names for
679+
both arguments and the clause as a whole (in the prefix style)
680+
681+
```scala
682+
// Named typeclass with named context parameter
683+
given listOrd: [A] => (ord: Ord[A]) => Ord[List[A]]:
684+
def compare(x: List[A], y: List[A]) = ...
685+
686+
// Named alias with named context parameter
687+
given listOrd: [A] => (ord: Ord[A]) => Ord[List[A]] =
688+
ListOrd[A]()
689+
```
690+
The new syntax fits exactly the approach of seeing conditional givens as implications: For instance,
691+
```scala
692+
[A] => Ord[A] => Ord[List[A]]
693+
```
694+
can be read as:
695+
696+
> If A is a type, then if `A` is `Ord`, then `List[A]` is `Ord`.
697+
698+
I think this is overall the cleanest proposal. For completeness here is the delta
699+
in the syntax description:
700+
```
701+
GivenDef ::= [id ':'] GivenSig
702+
GivenSig ::= GivenType ([‘=’ Expr] | TemplateBody)
703+
| ConstrApps TemplateBody
704+
| GivenConditional '=>' GivenSig
705+
GivenConditional ::= DefTypeParamClause | DefTermParamClause | '(' FunArgTypes ')'
706+
GivenType ::= AnnotType {id [nl] AnnotType}
707+
```
708+
This would also give a more regular and familiar syntax to by-name givens:
709+
```scala
710+
var ctx = ...
711+
given () => Context = ctx
712+
```
713+
Indeed, since we know `=>` means `?=>` in givens, this defines a value
714+
of type `() ?=> Context`, which is exactly the same as a by-name parameter type.
715+
716+
717+
**Possible ambiguities**
718+
719+
- If one wants to define a given for an a actual function type (which is probably not advisable in practice), one needs to enclose the function type in parentheses, i.e. `given ([A] => F[A])`. This is true in the currently implemented syntax and stays true for all discussed change proposals.
720+
721+
- The double meaning of `:` with optional prefix names is resolved as usual. A `:` at the end of a line starts a nested definition block. If for some obscure reason one wants to define
722+
a named given on multiple lines, one has to format it as follows:
723+
```scala
724+
given intOrd
725+
: Ord = ...
726+
727+
given intOrd
728+
: Ord:
729+
def concat(x: Int, y: Int) = ...
730+
```
731+
732+
Finally, for systematic comparison, here is the listing of all 9x2 cases discussed previously with the proposed alternative syntax. Only the 3rd, 6th, and 9th case are different from what was shown before.
733+
734+
Unnamed:
735+
736+
```scala
737+
// Simple typeclass
738+
given Ord[Int]:
739+
def compare(x: Int, y: Int) = ...
740+
741+
// Parameterized typeclass with context bound
742+
given [A: Ord] => Ord[List[A]]:
743+
def compare(x: List[A], y: List[A]) = ...
744+
745+
// Parameterized typeclass with context parameter
746+
given [A] => Ord[A] => Ord[List[A]]:
747+
def compare(x: List[A], y: List[A]) = ...
748+
749+
// Simple alias
750+
given Ord[Int] = IntOrd()
751+
752+
// Parameterized alias with context bound
753+
given [A: Ord] => Ord[List[A]] =
754+
ListOrd[A]()
755+
756+
// Parameterized alias with context parameter
757+
given [A] => Ord[A] => Ord[List[A]] =
758+
ListOrd[A]()
759+
760+
// Concrete class instance
761+
given Context()
762+
763+
// Abstract or deferred given
764+
given Context = deferred
765+
766+
// By-name given
767+
given () => Context = curCtx
768+
```
769+
Named:
770+
771+
```scala
772+
// Simple typeclass
773+
given intOrd: Ord[Int]:
774+
def compare(x: Int, y: Int) = ...
775+
776+
// Parameterized typeclass with context bound
777+
given listOrd: [A: Ord] => Ord[List[A]]:
778+
def compare(x: List[A], y: List[A]) = ...
779+
780+
// Parameterized typeclass with context parameter
781+
given listOrd: [A] => Ord[A] => Ord[List[A]]:
782+
def compare(x: List[A], y: List[A]) = ...
783+
784+
// Simple alias
785+
given intOrd: Ord[Int] = IntOrd()
786+
787+
// Parameterized alias with context bound
788+
given listOrd: [A: Ord] => Ord[List[A]] =
789+
ListOrd[A]()
790+
791+
// Parameterized alias with context parameter
792+
given listOrd: [A] => Ord[A] => Ord[List[A]] =
793+
ListOrd[A]()
794+
795+
// Concrete class instance
796+
given context: Context()
797+
798+
// Abstract or deferred given
799+
given context: Context = deferred
800+
801+
// By-name given
802+
given context: () => Context = curCtx
803+
```
804+
643805
## Summary
644806

645807
The proposed set of changes removes awkward syntax and makes dealing with context bounds and givens a lot more regular and pleasant. In summary, the proposed changes are:

0 commit comments

Comments
 (0)