2023/05/29
During some research on build systems, I began re-reading Build Systems a la Carte (repo, paper). I found the Haskell even more confusing than I remembered, so I set out to translate the Haskell types to Typescript, Rust, and other type systems I know. When I got into the translation, I realized I only know enough to get the vibe of what Haskell code’s saying :/.
The following are my notes on the 2021 open-source update to Learn You A Haskell for Great Good.
Starting out
https://learnyouahaskell.github.io/starting-out.html#an-intro-to-lists
What is the underlying datatype of a list? A linked list? Growable Vec<T>?
++:
I already need a language server for figuring out operator definitions.
List comprehension syntax:
[ output var | var <- inputSet]
-- ^^^^^^^^^
-- the output function
-- also valid:
[ output var | var <- inputSet, filterFn var, otherFilterFn var]
-- also valid:
triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]
Lists are homogenous, tuples can be heterogenous.
Range syntax seems to be the same as Rust.
Types & Typeclasses
The
[Char]type is synonymous withString
-- function type declaration addThree :: Int -> Int -> Int -> Int addThree x y z = x + y + z -- fn definition
Looks like functions are curried.
Fun fact: Haskell was named after Haskell Curry
A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes.
So like a typescript interface or a rust trait
Everything before the => symbol is called a class constraint.
Syntax in Functions
Looks like hitting an un-handled branch of pattern matching throws an exception. There are probably lints for such omissions.
Looks like _ is used as the ignored variable.
Guards are indicated by pipes that follow a function’s name and its parameters. Usually, they’re indented a bit to the right and lined up. A guard is basically a boolean expression. If it evaluates to True, then the corresponding function body is used. If it evaluates to False, checking drops through to the next guard and so on.
Neat!
wherebindings are a syntactic construct that let you bind to variables at the end of a function and the whole function can see them, including all the guards.letbindings let you bind to variables anywhere and are expressions themselves, but are very local, so they don’t span across guards.
x = let a = 1 in a + 1 -- x == 2
If we want to bind to several variables inline […] we can separate them with semicolons.
(let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar) -- (6000000,"Hey there!")
case expressions seem to be directly analogous to rust’s match expressions:
case expression of pattern -> result
pattern -> result
pattern -> result
...
Higher order functions
Infix functions can also be partially applied by using sections. To section an infix function, simply surround it with parentheses and only supply a parameter on one side. That creates a function that takes one parameter and then applies it to the side that’s missing an operand.
divideByTen :: (Floating a) => a -> a divideByTen = (/10)
“lambdas” = anonymous fns; written like
-- v starting \
(\arg -> result expr)
-- ^ ^ always parenthesized
[Haskell does] function composition with the . function, which is defined like so:
(.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (g x)
($) :: (a -> b) -> a -> b f $ x = f xWhereas normal function application (putting a space between two things) has a really high precedence, the
$function has the lowest precedence. Function application with a space is left-associative (sof a b cis the same as((f a) b) c)), function application with$is right-associative. … Most of the time,$’s a convenience function so that we don’t have to write so many parentheses. When a$is encountered, the expression on its right is applied as the parameter to the function on its left. How aboutsqrt 3 + 4 + 9? This adds together 9, 4 and the square root of 3. If we want to get the square root of 3 + 4 + 9, we’d have to writesqrt (3 + 4 + 9)or if we use$we can write it assqrt $ 3 + 4 + 9because$has the lowest precedence of any operator. That’s why you can imagine a$being sort of the equivalent of writing an opening parenthesis and then writing a closing one on the far right side of the expression.
^ this clears up a lot!
Modules
Note: the Prelude module is imported by default.
Note: module paths are separated by .s, e.g. Data.List.
import Data.List (nub, sort)
import Data.List hiding (nub)
import qualified Data.Map
-- if we want to reference Data.Map's filter function, we have to
-- do `Data.Map.filter`
import qualified Data.Map as M -- `M.filter`
module ModName ( -- must be same name as file.hs? MUST be capitalized
exported
, fn
, names
, Thing(..) -- all the value constructors for Thing
) where
Multi-module groups are directories of ModName.hs files, imported like import GroupName.ModName.
making our own types and typeclasses
if we add deriving (Show) at the end of a data declaration, Haskell automagically makes that type part of the Show typeclass.
data Person = Person { firstName :: String , lastName :: String , age :: Int , height :: Float , phoneNumber :: String , flavor :: String } deriving (Show)
type constructors can take types as parameters to produce new types
data Maybe a = Nothing | Just a -- type ^ parameter
A type can be made an instance of a typeclass if it supports that behavior. Example: the
Inttype is an instance of theEqtypeclass because theEqtypeclass defines behavior for stuff that can be equated.
When I talk about concrete types I mean like fully applied types
So concrete types don’t have any free type parameters, but type constructors may have further free type parameters.
class Eq equatable where
...
^ this is how to define a typeclass, noting that equatable is a type parameter.
class (Eq a) => Num a where
...
instance Eq (Maybe m) where
...
^ defining instances / implementations of a typeclass.
the
Functortypeclass, which is basically for things that can be mapped over. …class Functor f where fmap :: (a -> b) -> f a -> f bthe f is not a concrete type (a type that a value can hold, like Int, Bool or Maybe String), but a type constructor that takes one type parameter.
This is enough for me to read further, so I’m stopping for now.