diff --git a/src/part-1/chapter-2.html b/src/part-1/chapter-2.html index 78db0af..2ff8834 100644 --- a/src/part-1/chapter-2.html +++ b/src/part-1/chapter-2.html @@ -38,10 +38,10 @@
may return True
, False
, or _|_
; the latter meaning that it would never terminate.
Interestingly, once you accept the bottom as part of the type system, it is convenient to treat every runtime error as a bottom, and even allow functions to return the bottom explicitly. The latter is usually done using the expression undefined
, as in:
f :: Bool -> Bool - f x = undefined+f x = undefined
This definition type checks because undefined
evaluates to bottom, which is a member of any type, including Bool
. You can even write:
f :: Bool -> Bool - f = undefined+f = undefined
(without the x
) because the bottom is also a member of the type Bool->Bool
.
Functions that may return bottom are called partial, as opposed to total functions, which return valid results for every possible argument.
Because of the bottom, you’ll see the category of Haskell types and functions referred to as Hask rather than Set. From the theoretical point of view, this is the source of never-ending complications, so at this point I will use my butcher’s knife and terminate this line of reasoning. From the pragmatic point of view, it’s okay to ignore non-terminating functions and bottoms, and treat Hask as bona fide Set (see Bibliography at the end).
@@ -81,31 +81,31 @@int f44() { return 44; }
You might think of this function as taking “nothing”, but as we’ve just seen, a function that takes “nothing” can never be called because there is no value representing “nothing.” So what does this function take? Conceptually, it takes a dummy value of which there is only one instance ever, so we don’t have to mention it explicitly. In Haskell, however, there is a symbol for this value: an empty pair of parentheses, ()
. So, by a funny coincidence (or is it a coincidence?), the call to a function of void looks the same in C++ and in Haskell. Also, because of the Haskell’s love of terseness, the same symbol ()
is used for the type, the constructor, and the only value corresponding to a singleton set. So here’s this function in Haskell:
f44 :: () -> Integer - f44 () = 44+f44 () = 44
The first line declares that f44
takes the type ()
, pronounced “unit,” into the type Integer
. The second line defines f44
by pattern matching the only constructor for unit, namely ()
, and producing the number 44. You call this function by providing the unit value ()
:
f44 ()
Notice that every function of unit is equivalent to picking a single element from the target type (here, picking the Integer
44). In fact you could think of f44
as a different representation for the number 44. This is an example of how we can replace explicit mention of elements of a set by talking about functions (arrows) instead. Functions from unit to any type A are in one-to-one correspondence with the elements of that set A.
What about functions with the void
return type, or, in Haskell, with the unit return type? In C++ such functions are used for side effects, but we know that these are not real functions in the mathematical sense of the word. A pure function that returns unit does nothing: it discards its argument.
Mathematically, a function from a set A to a singleton set maps every element of A to the single element of that singleton set. For every A there is exactly one such function. Here’s this function for Integer
:
fInt :: Integer -> () - fInt x = ()+fInt x = ()
You give it any integer, and it gives you back a unit. In the spirit of terseness, Haskell lets you use the wildcard pattern, the underscore, for an argument that is discarded. This way you don’t have to invent a name for it. So the above can be rewritten as:
fInt :: Integer -> () - fInt _ = ()+fInt _ = ()
Notice that the implementation of this function not only doesn’t depend on the value passed to it, but it doesn’t even depend on the type of the argument.
Functions that can be implemented with the same formula for any type are called parametrically polymorphic. You can implement a whole family of such functions with one equation using a type parameter instead of a concrete type. What should we call a polymorphic function from any type to unit type? Of course we’ll call it unit
:
unit :: a -> () - unit _ = ()+unit _ = ()
In C++ you would write this function as:
template+void unit(T) {}- void unit(T) {}
Next in the typology of types is a two-element set. In C++ it’s called bool
and in Haskell, predictably, Bool
. The difference is that in C++ bool
is a built-in type, whereas in Haskell it can be defined as follows:
data Bool = True | False
(The way to read this definition is that Bool
is either True
or False
.) In principle, one should also be able to define a Boolean type in C++ as an enumeration:
enum bool { - true, - false - };+ true, + false +};
but C++ enum
is secretly an integer. The C++11 “enum class
” could have been used instead, but then you would have to qualify its values with the class name, as in bool::true
and bool::false
, not to mention having to include the appropriate header in every file that uses it.
Pure functions from Bool
just pick two values from the target type, one corresponding to True
and another to False
.
Functions to Bool
are called predicates. For instance, the Haskell library Data.Char
is full of predicates like isAlpha
or isDigit
. In C++ there is a similar library that defines, among others,
isalpha
and isdigit
, but these return an int
rather than a Boolean. The actual predicates are defined in std::ctype
and have the form ctype::is(alpha, c)
, ctype::is(digit, c)
, etc.
(a, b) -> c
It's trivial to convert between the two representations, and the two (higher-order) functions that do it are called, unsurprisingly, curry
and uncurry
:
curry :: ((a, b)->c) -> (a->b->c) - curry f a b = f (a, b)+curry f a b = f (a, b)
and
uncurry :: (a->b->c) -> ((a, b)->c) uncurry f (a, b) = f a b
Notice that curry
is the factorizer for the universal construction of the function object. This is especially apparent if it's rewritten in this form:
factorizer :: ((a, b)->c) -> (a->(b->c)) - factorizer g = \a -> (\b -> g (a, b))+factorizer g = \a -> (\b -> g (a, b))
(As a reminder: A factorizer produces the factorizing function from a candidate.)
In non-functional languages, like C++, currying is possible but nontrivial. You can think of multi-argument functions in C++ as corresponding to Haskell functions taking tuples (although, to confuse things even more, in C++ you can define functions that take an explicit std::tuple
, as well as variadic functions, and functions taking initializer lists).
You can partially apply a C++ function using the template std::bind
. For instance, given a function of two strings:
you can define a function of one string:
using namespace std::placeholders; - auto greet = std::bind(catstr, "Hello ", _1); - std::cout << greet("Haskell Curry");+auto greet = std::bind(catstr, "Hello ", _1); +std::cout << greet("Haskell Curry");
Scala, which is more functional than C++ or Java, falls somewhere in between. If you anticipate that the function you’re defining will be partially applied, you define it with multiple argument lists:
def catstr(s1: String)(s2: String) = s1 + s2
Of course that requires some amount of foresight or prescience on the part of a library writer.
@@ -202,7 +202,7 @@ eval (f, x) = f xFinally, we come to the meaning of the absurd
function:
absurd :: Void -> a
Considering that Void
translates into false, we get:
false ⇒ a+
false ⇒ a
Anything follows from falsehood (ex falso quodlibet). Here's one possible proof (implementation) of this statement (function) in Haskell:
absurd (Void a) = absurd a
where Void
is defined as:
It states that an empty list []
is the unit element, and list concatenation (++)
is the binary operation.
As we have seen, a list of type a
corresponds to a free monoid with the set a
serving as generators. The set of natural numbers with multiplication is not a free monoid, because we identify lots of products. Compare for instance:
2 * 3 = 6 - [2] ++ [3] = [2, 3] // not the same as [6]+[2] ++ [3] = [2, 3] // not the same as [6]
That was easy, but the question is, can we perform this free construction in category theory, where we are not allowed to look inside objects? We'll use our workhorse: the universal construction.
The second interesting question is, can any monoid be obtained from some free monoid by identifying more than the minimum number of elements required by the laws? I'll show you that this follows directly from the universal construction.
diff --git a/src/part-2/chapter-4.html b/src/part-2/chapter-4.html index 2c22c0f..ed60c12 100644 --- a/src/part-2/chapter-4.html +++ b/src/part-2/chapter-4.html @@ -44,7 +44,7 @@We have already seen this contravariant functor in Haskell. We called it Op
:
type Op a x = x -> a
instance Contravariant (Op a) where - contramap f h = h . f+ contramap f h = h . f
Finally, if we let both objects vary, we get a profunctor C(-, =)
, which is contravariant in the first argument and covariant in the second (to underline the fact that the two arguments may vary independently, we use a double dash as the second placeholder). We have seen this profunctor before, when we talked about functoriality:
instance Profunctor (->) where dimap ab cd bc = cd . bc . ab @@ -77,7 +77,7 @@We will see later that a natural transformation from
C(a, -)
to any Set-valued functor always exists (Yoneda's lemma) but it is not necessarily invertible.Let me give you an example in Haskell with the list functor and
Int
asa
. Here's a natural transformation that does the job:alpha :: forall x. (Int -> x) -> [x] - alpha h = map h [12]+alpha h = map h [12]
I have arbitrarily picked the number 12 and created a singleton list with it. I can then fmap
the function h
over this list and get a list of the type returned by h
. (There are actually as many such transformations as there are list of integers.)
The naturality condition is equivalent to the composability of map
(the list version of fmap
):
map f (map h [12]) = map (f . h) [12]@@ -86,18 +86,18 @@
You might think of retrieving an x
from the list, e.g., using head
, but that won't work for an empty list. Notice that there is no choice for the type a
(in place of Int
) that would work here. So the list functor is not representable.
Remember when we talked about Haskell (endo-) functors being a little like containers? In the same vein we can think of representable functors as containers for storing memoized results of function calls (the members of hom-sets in Haskell are just functions). The representing object, the type a
in C(a, -)
, is thought of as the key type, with which we can access the tabulated values of a function. The transformation we called α is called tabulate
, and its inverse, β, is called index
. Here's a (slightly simplified) Representable
class definition:
class Representable f where - type Rep f :: * - tabulate :: (Rep f -> x) -> f x - index :: f x -> Rep f -> x+ type Rep f :: * + tabulate :: (Rep f -> x) -> f x + index :: f x -> Rep f -> x
Notice that the representing type, our a
, which is called Rep f
here, is part of the definition of Representable
. The star just means that Rep f
is a type (as opposed to a type constructor, or other more exotic kinds).
Infinite lists, or streams, which cannot be empty, are representable.
data Stream x = Cons x (Stream x)
You can think of them as memoized values of a function taking an Integer
as an argument. (Strictly speaking, I should be using non-negative natural numbers, but I didn't want to complicate the code.)
To tabulate
such a function, you create an infinite stream of values. Of course, this is only possible because Haskell is lazy. The values are evaluated on demand. You access the memoized values using index
:
instance Representable Stream where - type Rep Stream = Integer - tabulate f = Cons (f 0) (tabulate (f . (+1))) - index (Cons b bs) n = if n == 0 then b else index bs (n - 1)+ type Rep Stream = Integer + tabulate f = Cons (f 0) (tabulate (f . (+1))) + index (Cons b bs) n = if n == 0 then b else index bs (n - 1)
It's interesting that you can implement a single memoization scheme to cover a whole family of functions with arbitrary return types.
Representability for contravariant functors is similarly defined, except that we keep the second argument of C(-, a)
fixed. Or, equivalently, we may consider functors from Cop to Set, because Cop(a, -)
is the same as C(-, a)
.
There is an interesting twist to representability. Remember that hom-sets can internally be treated as exponential objects, in cartesian closed categories. The hom-set C(a, x)
is equivalent to xa
, and for a representable functor F
we can write: