import { open } from "node:fs/promises" export function firstThenApply<T, U>( array: T[], predicate: (x: T) => boolean, consumer: (x: T) => U | undefined ): U | undefined { const first = array.find(predicate) if (first) { return consumer(first) } return undefined } export function* powersGenerator(base: bigint): Generator<bigint> { for (let power = 1n; true; power *= base) { yield power } } export async function meaningfulLineCount(filename: string): Promise<number> { let count = 0 const file = await open(filename, "r") for await (const line of file.readLines()) { const trimmed = line.trim() if (trimmed && !trimmed.startsWith("#")) { count++ } } return count } interface Sphere { kind: "Sphere" readonly radius: number } interface Box { kind: "Box" readonly width: number readonly length: number readonly depth: number } export type Shape = Sphere | Box export function volume(shape: Shape): number { switch (shape.kind) { case "Sphere": return (4 / 3) * Math.PI * shape.radius ** 3 case "Box": return shape.width * shape.length * shape.depth } } export function surfaceArea(shape: Shape): number { switch (shape.kind) { case "Sphere": return 4 * Math.PI * shape.radius ** 2 case "Box": const { length, width, depth } = shape return 2 * (length * width + length * depth + width * depth) } } export interface BinarySearchTree<T> { size(): number insert(data: T): BinarySearchTree<T> contains(data: T): boolean inorder(): Iterable<T> } export class Empty<T> implements BinarySearchTree<T> { size(): number { return 0 } insert(data: T): BinarySearchTree<T> { return new Node<T>(data, new Empty<T>(), new Empty<T>()) } contains(data: T): boolean { return false } *inorder(): Iterable<T> {} toString(): string { return "()" } } class Node<T> implements BinarySearchTree<T> { constructor( private readonly data: T, private readonly left: BinarySearchTree<T>, private readonly right: BinarySearchTree<T> ) {} size(): number { return this.left.size() + this.right.size() + 1 } insert(data: T): BinarySearchTree<T> { if (data < this.data) { return new Node(this.data, this.left.insert(data), this.right) } else if (data > this.data) { return new Node(this.data, this.left, this.right.insert(data)) } else { return this } } contains(data: T): boolean { if (data < this.data) { return this.left.contains(data) } else if (data > this.data) { return this.right.contains(data) } else { return true } } *inorder(): Iterable<T> { yield* this.left.inorder() yield this.data yield* this.right.inorder() } toString(): string { const left = this.left instanceof Empty ? "" : this.left.toString() const right = this.right instanceof Empty ? "" : this.right.toString() return `(${left}${this.data}${right})` } }
module Exercises ( firstThenApply, powers, meaningfulLineCount, Shape(..), BST(Empty), volume, surfaceArea, size, contains, insert, inorder ) where import qualified Data.Map as Map import Data.List(isPrefixOf, find) import Data.Char(isSpace) firstThenApply :: [a] -> (a -> Bool) -> (a -> b) -> Maybe b firstThenApply xs p f = f <$> find p xs powers :: Integral a => a -> [a] powers base = map (base^) [0..] meaningfulLineCount :: FilePath -> IO Int meaningfulLineCount filePath = do document <- readFile filePath let allWiteSpace = all isSpace trimStart = dropWhile isSpace isMeaningful line = not (allWiteSpace line) && not ("#" `isPrefixOf` (trimStart line)) return $ length $ filter isMeaningful $ lines document data Shape = Sphere Double | Box Double Double Double deriving (Eq, Show) volume :: Shape -> Double volume (Sphere r) = (4 / 3) * pi * (r ^ 3) volume (Box w l d) = w * l * d surfaceArea :: Shape -> Double surfaceArea (Sphere r) = 4 * pi * (r ^ 2) surfaceArea (Box w l d) = 2 * ((w * l) + (w * d) + (l * d)) data BST a = Empty | Node a (BST a) (BST a) insert :: Ord a => a -> BST a -> BST a insert x Empty = Node x Empty Empty insert x (Node y left right) | x < y = Node y (insert x left) right | x > y = Node y left (insert x right) | otherwise = Node y left right contains :: Ord a => a -> BST a -> Bool contains _ Empty = False contains x (Node y left right) | x < y = contains x left | x > y = contains x right | otherwise = True size :: BST a -> Int size Empty = 0 size (Node _ left right) = 1 + size left + size right inorder :: BST a -> [a] inorder Empty = [] inorder (Node x left right) = inorder left ++ [x] ++ inorder right instance (Show a) => Show (BST a) where show :: Show a => BST a -> String show Empty = "()" show (Node x Empty Empty) = "(" ++ show x ++ ")" show (Node x Empty right) = "(" ++ show x ++ show right ++ ")" show (Node x left Empty) = "(" ++ show left ++ show x ++ ")" show (Node x left right) = "(" ++ show left ++ show x ++ show right ++ ")"
Answer: A tree whose elements are all of type $t$ is either (1) empty or (2) a node consisting of a value of type $t$ and a list of subtrees, each of which is a tree of elements of type $t$.
$$ \frac{}{\textsf{Empty}\!:\textsf{Tree}(t)} \quad\quad \frac{v\!:t \quad c\!:\textsf{Tree}(t)^*}{\textsf{Node}\:v\:c\!:\,\textsf{Tree}(t)} $$Answer: (This one is kind of tricky since the cases are done on the “second argument”):
$\begin{array}{l} \textsf{exp}\!: \textsf{Nat} \rightarrow \textsf{Nat} \rightarrow \textsf{Nat} \\ \textsf{exp}\;m\;0 = 1 \\ \textsf{exp}\;m\;(\textsf{s}\,n) = \textsf{times}\;(\textsf{exp}\;m\;n)\;m \\ \end{array}$
Answer: (Note how both types have three inhabitants each, but the inhabitants of the sum type are tagged):
(a) These three: $\textsf{tag}_1\textsf{true}$, $\textsf{tag}_2\textsf{false}$, $\textsf{tag}_2()$Answer:
(a) These two: $(\textsf{true}, ())$ and $(\textsf{false}, ())$Answer: It seems like there are two. First, many (most?) people do not understand basic concepts like encodings and normalization, and thus simple operations like reversal and uppercasing tend not to work the way these people expect them to. Second, different programming languages handle strings differently, so there tends to be no agreement for what a string even is.
Answer: Well, if you tried a naive approach, you would see that the first part, $(\lambda x.(x\:x))$, would have to be a function, so you would give that the type $t \to u$. But that function is applied to itself, so $t$ would be $t \to u$. This expansion never ends ($t \to t \to t \to \ldots$), so there’s no clean type for this expression. But think about what this expression is doing. Let’s “run” it:
$(\lambda x.(x\:x))(\lambda x.(x\:x)) = (\lambda x.(x\:x))(\lambda x.(x\:x)) = (\lambda x.(x\:x))(\lambda x.(x\:x)) = \ldots$
It never ends! So we can give it the type $\textsf{Never}$, which is used in programming languages for these situations. ($\textsf{Void}$ is also an acceptable answer.)
Answer: $\neg A\,x$
Answer: A pure function is one without side effects, where the output depends only on the input and does not modify any external state, and gives the same output for each input no matter when and how many times it is called. We care because pure functions are thread-safe, easier to test, easier to prove correct, easier to parallelize, and often more amenable to optimization.
Answer: Haskell has monadic types whose instances are descriptions of stateful and I/O operations—not the operations themselves. Haskell prevents impure operations to take place inside a pure function. The side effects are all run after main is evaluated.
| or & is closer to the idea of “subclassing” or “inheritance” from Python? Why? (An example will help!)
Answer: Intersection (&) is closer. Here‘s a example, courtesy of ChatGPT:
type Animal = { eat(): void } type Barker = { bark(): void } type Dog = Animal & Barker const d: Dog = { eat() { console.log("nom nom") }, bark() { console.log("woof") }, }