more on induction + implement language switcher
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
07484d5812
commit
a248240a6a
5 changed files with 277 additions and 13 deletions
|
@ -338,6 +338,73 @@ table.table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tabbing
|
||||||
|
.language-switcher-choice {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabbed {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
|
||||||
|
ul.tabs {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
list-style: none;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
border-bottom: 1px solid lightgray;
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
.tab-content {
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@for $i from 1 through 5 {
|
||||||
|
$colors: [green, red, blue, yellow, purple];
|
||||||
|
|
||||||
|
.language-switcher-choice:nth-of-type(#{$i}):checked {
|
||||||
|
outline: 2px solid nth($colors, $i);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:has(.language-switcher-choice:nth-of-type(#{$i}):checked) .tabbed {
|
||||||
|
.tabs .tab:nth-child(#{$i}) {
|
||||||
|
border-bottom: 3px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs .tab:not(:nth-child(#{$i})) {
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents .tab-content:nth-child(#{$i}) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents .tab-content:not(:nth-child(#{$i})) {
|
||||||
|
// text-shadow: 2px 2px nth($colors, $i);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post container
|
||||||
|
|
||||||
.post-container {
|
.post-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -375,7 +442,7 @@ table.table {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.post-content {
|
.post-content {
|
||||||
ul {
|
ul:not(.tabs) {
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,6 +470,12 @@ table.table {
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlight,
|
||||||
|
details {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.logseq-post {
|
&.logseq-post {
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
+++
|
+++
|
||||||
title = "Inductive Types"
|
title = "Inductive Types"
|
||||||
slug = "inductive-types"
|
|
||||||
date = 2023-03-26
|
date = 2023-03-26
|
||||||
tags = ["type-theory"]
|
tags = ["type-theory"]
|
||||||
math = true
|
math = true
|
||||||
draft = true
|
draft = true
|
||||||
|
|
||||||
|
language_switcher_languages = ["ocaml", "python"]
|
||||||
+++
|
+++
|
||||||
|
|
||||||
There's a feature common to many functional languages, the ability to have
|
There's a feature common to many functional languages, the ability to have
|
||||||
algebraic data types. It might look something like this (OCaml syntax):
|
algebraic data types. It might look something like this:
|
||||||
|
|
||||||
|
{{< language-switcher >}}
|
||||||
```ocaml
|
```ocaml
|
||||||
type bool =
|
type bool =
|
||||||
| True
|
| True
|
||||||
|
@ -26,6 +28,20 @@ that has two _constructors_, or ways to create this type. The constructors are
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Literal
|
||||||
|
MyBool = Literal[True] | Literal[False]
|
||||||
|
```
|
||||||
|
{{</ language-switcher >}}
|
||||||
|
|
||||||
|
> **Note:** I'm using an experimental language switcher. It's implemented in
|
||||||
|
> pure CSS using a feature called the [`:has` pseudo-class][has]. As of writing,
|
||||||
|
> all major browsers _except_ Firefox has it implemented and enabled by default.
|
||||||
|
> For Firefox there does exist a feature flag in about:config, but your mileage
|
||||||
|
> may vary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Many languages have this feature, under different names. Tagged unions, variant
|
Many languages have this feature, under different names. Tagged unions, variant
|
||||||
types, enumerations, but they all reflect a basic idea: a type with a limited
|
types, enumerations, but they all reflect a basic idea: a type with a limited
|
||||||
set of variants.
|
set of variants.
|
||||||
|
@ -41,12 +57,21 @@ unknown value of type `Boolean`, you know it can only take one of two values.
|
||||||
There's actually nothing special about boolean itself. I could just as easily
|
There's actually nothing special about boolean itself. I could just as easily
|
||||||
define a new type, like this:
|
define a new type, like this:
|
||||||
|
|
||||||
|
{{< language-switcher >}}
|
||||||
```ocaml
|
```ocaml
|
||||||
type WeirdType =
|
type WeirdType =
|
||||||
| Foo
|
| Foo
|
||||||
| Bar
|
| Bar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Literal
|
||||||
|
WeirdType = Literal['foo'] | Literal['bar']
|
||||||
|
```
|
||||||
|
{{</ language-switcher >}}
|
||||||
|
|
||||||
Because this type can only have two values, it's _semantically_ equivalent to
|
Because this type can only have two values, it's _semantically_ equivalent to
|
||||||
the `Boolean` type. I could use it anywhere I would typically use `Boolean`.
|
the `Boolean` type. I could use it anywhere I would typically use `Boolean`.
|
||||||
|
|
||||||
|
@ -65,11 +90,20 @@ You can make any _finite_ type like this: just create an algebraic data type
|
||||||
with unit constructors, and the result is a type with a finite cardinality. If I
|
with unit constructors, and the result is a type with a finite cardinality. If I
|
||||||
wanted to make a unit type for example:
|
wanted to make a unit type for example:
|
||||||
|
|
||||||
|
{{< language-switcher >}}
|
||||||
```ocaml
|
```ocaml
|
||||||
type unit =
|
type unit =
|
||||||
| Unit
|
| Unit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Literal
|
||||||
|
Unit = Literal[None]
|
||||||
|
```
|
||||||
|
{{</ language-switcher >}}
|
||||||
|
|
||||||
There's only one way to ever construct something of this type, so the
|
There's only one way to ever construct something of this type, so the
|
||||||
cardinality of this type would be 1.
|
cardinality of this type would be 1.
|
||||||
|
|
||||||
|
@ -83,6 +117,7 @@ called structural matching in some languages).
|
||||||
Let's see an example. Suppose I have a type with three values, defined like
|
Let's see an example. Suppose I have a type with three values, defined like
|
||||||
this:
|
this:
|
||||||
|
|
||||||
|
{{< language-switcher >}}
|
||||||
```ocaml
|
```ocaml
|
||||||
type direction =
|
type direction =
|
||||||
| Left
|
| Left
|
||||||
|
@ -90,10 +125,19 @@ type direction =
|
||||||
| Right
|
| Right
|
||||||
```
|
```
|
||||||
|
|
||||||
If I was given a value with type `direction`, but I wanted to do different
|
---
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Literal
|
||||||
|
Direction = Literal['left'] | Literal['middle'] | Literal['right']
|
||||||
|
```
|
||||||
|
{{</ language-switcher >}}
|
||||||
|
|
||||||
|
If I was given a value with a type of direction, but I wanted to do different
|
||||||
things depending on exactly which direction it was, I could use _pattern
|
things depending on exactly which direction it was, I could use _pattern
|
||||||
matching_ like this:
|
matching_ like this:
|
||||||
|
|
||||||
|
{{< language-switcher >}}
|
||||||
```ocaml
|
```ocaml
|
||||||
let do_something_with (d : direction) =
|
let do_something_with (d : direction) =
|
||||||
match d with
|
match d with
|
||||||
|
@ -102,6 +146,22 @@ let do_something_with (d : direction) =
|
||||||
| Right -> do_this_if_right
|
| Right -> do_this_if_right
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```python
|
||||||
|
def do_something_with(d : Direction) -> str:
|
||||||
|
match inp:
|
||||||
|
case 'left': return do_this_if_left
|
||||||
|
case 'middle': return do_this_if_middle
|
||||||
|
case 'right': return do_this_if_right
|
||||||
|
case _: assert_never(inp)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** the `assert_never` is a static check for exhaustiveness. If we missed
|
||||||
|
a single one of the cases, a static type checker like [pyright] could catch it
|
||||||
|
and tell us which of the remaining cases there are.
|
||||||
|
{{</ language-switcher >}}
|
||||||
|
|
||||||
This gives me a way to discriminate between the different variants of
|
This gives me a way to discriminate between the different variants of
|
||||||
`direction`.
|
`direction`.
|
||||||
|
|
||||||
|
@ -109,7 +169,7 @@ This gives me a way to discriminate between the different variants of
|
||||||
> the `Boolean` type, called if-else. What would if-else look like if you wrote
|
> the `Boolean` type, called if-else. What would if-else look like if you wrote
|
||||||
> it as a function in this pattern-matching form?
|
> it as a function in this pattern-matching form?
|
||||||
|
|
||||||
## The Algebra of Types
|
## Constructing larger types
|
||||||
|
|
||||||
Finite-cardinality types like the ones we looked at just now are nice, but
|
Finite-cardinality types like the ones we looked at just now are nice, but
|
||||||
they're not super interesting. If you had a programming language with nothing
|
they're not super interesting. If you had a programming language with nothing
|
||||||
|
@ -131,24 +191,106 @@ contain themselves as a type).
|
||||||
|
|
||||||
You can see an example of this here:
|
You can see an example of this here:
|
||||||
|
|
||||||
|
{{< language-switcher >}}
|
||||||
```ocaml
|
```ocaml
|
||||||
type nat =
|
type nat =
|
||||||
| Suc of nat
|
| Suc of nat
|
||||||
| Zero
|
| Zero
|
||||||
```
|
```
|
||||||
|
{{</ language-switcher >}}
|
||||||
|
|
||||||
These are the natural numbers, which are defined inductively. Each number is
|
These are the natural numbers, which are defined inductively. Each number is
|
||||||
just represented by a data type that wraps 0 that number of times. So 3 would be
|
just represented by a data type that wraps 0 that number of times. So 3 would be
|
||||||
`Suc (Suc (Suc Zero))`.
|
`Suc (Suc (Suc Zero))`.
|
||||||
|
|
||||||
This data type is _inductive_ because the `Suc` case can contain arbitrarily
|
At this point you can probably see why these have _infinite_ cardinality: with
|
||||||
many `nat`s inside of it. This also means that if we want to talk about writing
|
the Suc case, you can keep wrapping nats as many times as you want!
|
||||||
any functions on `nat`, we just have to supply 2 cases instead of an infinite
|
|
||||||
number of cases:
|
One key observation here is that although the _cardinality_ of the entire type is
|
||||||
|
infinite, it only uses _two_ constructors to build it. This also means that if
|
||||||
|
we want to talk about writing any functions on `nat`, we just have to supply 2
|
||||||
|
cases instead of an infinite number of cases:
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
let is_even = fun (x : nat) ->
|
let rec is_even = fun (n : nat) ->
|
||||||
match x with
|
match n with
|
||||||
| Suc n -> not (is_even n)
|
|
||||||
| Zero -> true
|
| Zero -> true
|
||||||
|
| Suc n1 -> not (is_even n1)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Try it for yourself</summary>
|
||||||
|
|
||||||
|
If you've got an OCaml interpreter handy, try a couple values for yourself and
|
||||||
|
convince yourself that this accurately represents the naturals and an even
|
||||||
|
testing function:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
utop # is_even Zero;;
|
||||||
|
- : bool = true
|
||||||
|
utop # is_even (Suc Zero);;
|
||||||
|
- : bool = false
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a good way of making sure the functions you write make sense!
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Induction principle
|
||||||
|
|
||||||
|
Let's express this in the language of mathematical induction. If I have any
|
||||||
|
natural number:
|
||||||
|
|
||||||
|
- If the natural number is the base case of zero, then the `is_even` relation
|
||||||
|
automatically evaluates to true.
|
||||||
|
- If the natural number is a successor, invert the induction hypothesis (which is
|
||||||
|
what `is_even` evaluates to for the previous step, a.k.a whether or not the
|
||||||
|
previous natural number is even), since every even number is succeeded by
|
||||||
|
an odd number and vice versa.
|
||||||
|
|
||||||
|
Once these rules are followed, by induction we know that `is_even` can run on
|
||||||
|
any natural number ever. In code, this looks like:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
let is_even
|
||||||
|
(n_zero : bool)
|
||||||
|
(n_suc : nat -> bool)
|
||||||
|
(n : nat)
|
||||||
|
: bool =
|
||||||
|
match n with
|
||||||
|
| Zero -> n_zero
|
||||||
|
| Suc n1 -> n_suc n1
|
||||||
|
```
|
||||||
|
|
||||||
|
where n_zero defines what to do with the zero case, and n_suc defines what to do
|
||||||
|
with the successor case.
|
||||||
|
|
||||||
|
You might've noticed that this definition doesn't actually return any booleans.
|
||||||
|
That's because this is not actually the is_even function! This is a general
|
||||||
|
function that turns any natural into a boolean. In fact, we can go one step
|
||||||
|
further and generalize this to all types:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
let nat_transformer
|
||||||
|
(n_zero : 'a)
|
||||||
|
(n_suc : 'a -> 'a)
|
||||||
|
(n : nat)
|
||||||
|
: 'a =
|
||||||
|
match n with
|
||||||
|
| Zero -> n_zero
|
||||||
|
| Suc n1 -> n_suc n1
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's say I wanted to write a function that converts from our custom-defined nat
|
||||||
|
type into an OCaml integer. Using this constructor, that would look something
|
||||||
|
like this:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
let convert_nat = nat_transformer 0 (fun x -> x + 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TODO: Talk about https://counterexamples.org/currys-paradox.html
|
||||||
|
|
||||||
|
[has]: https://developer.mozilla.org/en-US/docs/Web/CSS/:has
|
||||||
|
[pyright]: https://github.com/microsoft/pyright
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ $style := resources.Get "sass/main.scss" | resources.ToCSS }}
|
{{ $style := resources.Get "sass/main.scss" | resources.ToCSS }}
|
||||||
<link rel="stylesheet" href="{{ $style.RelPermalink }}" />
|
<link rel="stylesheet" href="{{ $style.RelPermalink }}" crossorigin="anonymous" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -26,6 +26,27 @@
|
||||||
- {{ .ReadingTime }} min read
|
- {{ .ReadingTime }} min read
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
{{ if .Params.language_switcher_languages }}
|
||||||
|
<div class="has-warning">
|
||||||
|
warning! your browser does not support has
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="language_switcher_choices">
|
||||||
|
{{ range $index, $lang := .Page.Params.language_switcher_languages }}
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="language-switcher"
|
||||||
|
class="language-switcher-choice"
|
||||||
|
id="language-switcher-{{ $lang }}"
|
||||||
|
value="{{ $lang }}"
|
||||||
|
{{ if (eq $index 0) }}
|
||||||
|
checked
|
||||||
|
{{ end }}
|
||||||
|
>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<div class="post-container
|
<div class="post-container
|
||||||
{{ if .Params.logseq }}logseq-post{{ end }}
|
{{ if .Params.logseq }}logseq-post{{ end }}
|
||||||
">
|
">
|
||||||
|
|
28
layouts/shortcodes/language-switcher.html
Normal file
28
layouts/shortcodes/language-switcher.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{{ $_hugo_config := `{ "version": 1 }` }}
|
||||||
|
|
||||||
|
{{ $parts := split .Inner "---" }}
|
||||||
|
{{ $page := .Page }}
|
||||||
|
|
||||||
|
<div class="tabbed">
|
||||||
|
|
||||||
|
<ul class="tabs">
|
||||||
|
{{ range $index, $snippet := $parts }}
|
||||||
|
{{ $lang := index $page.Params.language_switcher_languages $index }}
|
||||||
|
<li class="tab">
|
||||||
|
<label for="language-switcher-{{ $lang }}">{{ $lang }}</label>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="contents">
|
||||||
|
{{ range $index, $snippet := $parts }}
|
||||||
|
<div class="tab-content">
|
||||||
|
{{ $snippet | markdownify }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* Thank you https://codepen.io/MPDoctor/pen/mpJdYe */}}
|
Loading…
Reference in a new issue