Learning Function Signatures in 3 Weeks/21 days
Table of Contents
- Function Signatures with Types: The Architect's Curriculum
- Week 1: The Foundation — Seeing Functions as Contracts
- Week 2: Abstraction & System Design
- Week 3: Mastery, Evaluation & Architectural Thinking
- Daily Practice Exercises (21 Days)
- Evaluation Rubric
- Final Note
Function Signatured for system design and developing complex systems without thinking about the details A function is not code. It is a contract. This reads: "There exists a contract named f. It accepts something of type A. It promises something of type B." Notice what is absent: no variables, no loops, no state. Only a promise. The Fundamental Axiom: If you know the Input type and the Output type, you know everything necessary to compose the function into a larger system. Before writing signatures, you must name your universe. Exercise: Rule: A primitive type carries no semantic weight beyond its structure. When a function needs multiple inputs or produces multiple outputs, you need product types. Tuple notation (anonymous, positional): Record notation (named, semantic): The critical difference: Exercise: Not all outputs are singular. A function may return one of several types. Exercise: Rule: Every union type is a decision point in your system. The consumer must handle every variant. A special case of sum types: presence or absence. Exercise: Architectural insight: Functions rarely operate on single values. They operate on collections. Exercise: Drill 1: Read this aloud. "Transform accepts a function from A to B, and a List of A, and promises a List of B." Drill 2: What is missing here? Answer: The parameter names. We know the types but not the semantics. Drill 3: Improve this signature. Improved: Generics allow signatures to abstract over concrete types. The Power: A generic signature makes a universal claim. Exercise: Functions that accept or return other functions. Reading complex signatures (right-associative): This is: Exercise: Primitive types are structurally correct but semantically dangerous. Exercise: Rule: If two values should never be accidentally swapped, they must have distinct types. Signatures must encode not just what is returned, but how the computation behaves. Exercise: Sometimes a generic type must support specific operations. Reading: Design using only signatures. No database schemas. No flowcharts. System insight: Notice how Architectural boundary: Key decision: Before: After — Step 1 (Add types): After — Step 2 (Add semantics): After — Step 3 (Add domain types): Exercise: Refactor these: Solutions: Systems are composed of function-shaped components. Middleware pattern: Pipeline pattern: Event handler pattern: Abstract over behavior, not just data. Reader pattern (dependency injection via types): State pattern: Continuation pattern: Implementation is a detail. If the signature is correct, the implementation can be changed without breaking the system. Types are the only documentation that cannot lie. Comments become stale. Signatures are enforced. A complex signature indicates a complex contract. Do not simplify the signature to hide complexity. Make the complexity explicit so it can be managed. Side effects must be visible in types. A function Compose signatures, not implementations. If Before accepting any signature, ask: Function Signatures with Types: The Architect's Curriculum
A 3-Week Mastery Program in Input-Output Contract Design
Week 1: The Foundation — Seeing Functions as Contracts
Day 1: The Core Intuition
f : A → B Day 2: Primitive Types
Int : whole numbers
Float : real numbers
String : ordered sequences of characters
Bool : true | false
Void : ()
Unit : a type with exactly one inhabitant
Never : a type with zero inhabitantstoString : Int → String
isPositive : Int → Bool
negate : Bool → BoolInt tells you nothing about whether it represents an age, a count, or an ID. Day 3: Composite Types — Products (Tuples & Records)
divide : (Float, Float) → Floatdivide : (dividend: Float, divisor: Float) → Float(Float, Float) says "two floats"(dividend: Float, divisor: Float) says "a division operation"createUser : (name: String, age: Int) → User
getCoordinates : () → (lat: Float, long: Float) Day 4: Composite Types — Sums (Unions & Enums)
Status : Loading | Success<T> | Error<E>parseInt : String → Int | ParseError
findUser : UserId → User | NotFound Day 5: Optional / Nullable Types
Option<T> : Some<T> | None
Maybe<T> : Just<T> | NothingfindById : UserId → Option<User>
head : List<T> → Option<T>Option forces the caller to acknowledge absence. It removes the billion-dollar mistake from your contracts. Day 6: Container Types — List, Map, Set
List<T> : ordered, duplicate-permitted sequence
Set<T> : unordered, duplicate-free collection
Map<K, V> : key-value associationfilter : (T → Bool) → List<T> → List<T>
groupBy : (T → K) → List<T> → Map<K, List<T>>
unique : List<T> → Set<T> Day 7: Week 1 Review — Signature Reading Drills
transform : (A → B) → List<A> → List<B>process : (String, Int) → Stringf : (String, String) → Boolauthenticate : (username: String, password: String) → AuthResult Week 2: Abstraction & System Design
Day 8: Generic Types (Parametric Polymorphism)
identity : T → T
pair : T → U → (T, U)
first : (T, U) → Tidentity : T → T states: "For all types T, this contract holds."swap : (T, U) → (U, T)
compose : (B → C) → (A → B) → (A → C) Day 9: Higher-Order Functions
map : (T → U) → List<T> → List<U>
filter : (T → Bool) → List<T> → List<T>
reduce : (Acc → T → Acc) → Acc → List<T> → AcccurriedAdd : Int → Int → IntInt → (Int → Int) — a function that takes an Int and returns a function.flip : (A → B → C) → (B → A → C)
on : (B → B → C) → (A → B) → (A → A → C) Day 10: Domain-Specific Types (Newtypes)
UserId : newtype String
Email : newtype String
Money : newtype Int
Password: newtype StringfindUser : UserId → Option<User>
sendEmail : Email → Body → Result<Email, SendError>
withdraw : AccountId → Money → Result<Balance, InsufficientFunds> Day 11: Effect Types — Result, IO, Option
Result<T, E> : computation that may fail with E
IO<T> : computation that interacts with the world
Option<T> : computation that may produce nothing
Task<T> : asynchronous computationreadFile : Path → IO<Result<String, FileError>>
fetchUser : UserId → Task<Result<User, NetworkError>>
parseConfig : String → Result<Config, ParseError> Day 12: Type Constraints
sort : Ord<T> => List<T> → List<T>
sum : Num<T> => List<T> → T
serialize : Json<T> => T → StringOrd<T> => means "This contract is valid for all T where an ordering relation exists." Day 13: System Design — Authentication System
// Types
UserId : newtype String
Email : newtype String
PasswordHash : newtype String
Token : newtype String
Credentials : (email: Email, password: String)
// Domain
hashPassword : String → PasswordHash
verifyPassword : String → PasswordHash → Bool
generateToken : UserId → SecretKey → Token
validateToken : Token → SecretKey → Result<UserId, InvalidToken>
// Services
register : Credentials → UserRepository → Result<UserId, EmailExists>
login : Credentials → UserRepository → SecretKey → Result<Token, AuthError>
authenticate : Token → SecretKey → Result<UserId, ExpiredToken>UserRepository appears as an explicit parameter. In signature-based design, dependencies are explicit inputs. Day 14: System Design — Payment System
// Types
Money : newtype Int
Currency : USD | EUR | GBP
AccountId : newtype String
TransactionId : newtype String
// Domain
MoneyAmount : (value: Money, currency: Currency)
// Services
credit : AccountId → MoneyAmount → Ledger → Result<TransactionId, LedgerError>
debit : AccountId → MoneyAmount → Ledger → Result<TransactionId, InsufficientFunds>
transfer : AccountId → AccountId → MoneyAmount → Ledger → Result<TransactionId, TransferError>
getBalance : AccountId → Ledger → MoneyAmount
exchange : MoneyAmount → Currency → ExchangeRate → MoneyAmountLedger is passed explicitly. This signature says nothing about whether it is in-memory, on-disk, or distributed. The contract remains stable. Week 3: Mastery, Evaluation & Architectural Thinking
Day 15: System Design — Chat System
// Types
UserId : newtype String
RoomId : newtype String
MessageId : newtype String
Timestamp : newtype Int
Content : newtype String
// Domain
Message : (id: MessageId, room: RoomId, author: UserId, content: Content, sentAt: Timestamp)
// Services
joinRoom : UserId → RoomId → ChatRoom → Result<Participant, RoomFull>
leaveRoom : UserId → RoomId → ChatRoom → ChatRoom
sendMessage : UserId → RoomId → Content → ChatRoom → Result<Message, NotInRoom>
getHistory : RoomId → Timestamp → ChatRoom → List<Message>
subscribe : RoomId → ChatRoom → Stream<Message>subscribe returns a Stream<Message> rather than List<Message>. The signature encodes the temporal nature of chat. Day 16: System Design — File Upload System
// Types
FileId : newtype String
FileName : newtype String
MimeType : newtype String
FileSize : newtype Int
Chunk : newtype Bytes
UploadId : newtype String
// Domain
FileMetadata : (id: FileId, name: FileName, mime: MimeType, size: FileSize)
UploadProgress : (uploadId: UploadId, bytesReceived: FileSize, totalBytes: FileSize)
// Services
initiateUpload : FileMetadata → Storage → Result<UploadId, StorageFull>
uploadChunk : UploadId → Chunk → Storage → Result<UploadProgress, UploadNotFound>
completeUpload : UploadId → Storage → Result<FileId, UploadIncomplete>
getFile : FileId → Storage → Result<File, FileNotFound>
deleteFile : FileId → Storage → Result<Void, FileNotFound>
generateUrl : FileId → Expiry → Storage → Result<Url, FileNotFound> Day 17: Signature Refactoring — From Vague to Precise
process(data) // vague, untypedprocess : String → Stringprocess : JsonString → JsonStringprocess : OrderPayload → Result<Confirmation, ValidationError>doThing : (String, String) → String
handle : (Int, Int) → Bool
getStuff : String → List<String>createInvoice : (customerId: CustomerId, orderId: OrderId) → Result<Invoice, InvoiceError>
authorizePayment : (amount: Money, accountId: AccountId) → Result<AuthCode, Declined>
searchProducts : Query → List<ProductSummary> Day 18: Higher-Order System Patterns
Middleware : (Request → Response) → (Request → Response)Pipeline : List<(T → Result<T, E>)> → T → Result<T, List<E>>Handler<E> : E → List<Command>
Projection<S, E> : S → E → S Day 19: Polymorphism & Typeclasses
// Typeclass definitions (constraints)
Eq<T> : T → T → Bool
Ord<T> : T → T → Ordering
Semigroup<T> : T → T → T
Monoid<T> : Semigroup<T> + empty : T
Functor<F> : (A → B) → F<A> → F<B>
Monad<M> : Functor<M> + pure : A → M<A> + flatMap : (A → M<B>) → M<A> → M<B>
// Usage
findMax : Ord<T> => List<T> → Option<T>
concatAll : Monoid<T> => List<T> → T
map : Functor<F> => (A → B) → F<A> → F<B> Day 20: Advanced Signature Patterns
Reader<R, A> : R → A
getUser : Reader<UserRepository, User>State<S, A> : S → (A, S)
increment : State<Counter, Unit>Cont<R, A> : (A → R) → R
withResource : Resource → (Resource → R) → R Day 21: The Architect's Mindset
The Final Axioms
f : A → B is pure. A function f : A → IO<B> is effectful. The difference is architectural.f : A → B and g : B → C, then g ∘ f : A → C is guaranteed to connect. The Design Checklist
Daily Practice Exercises (21 Days)
Week 1: Types & Notation
Day Exercise 1 Write signatures for: absolute value, string length, boolean negation 2 Write signatures using: Int, Float, String, Bool, Char 3 Convert these to record inputs: f(String, Int), g(Float, Float, Float)4 Write 5 functions that return union types 5 Rewrite 3 functions from Day 4 using Option<T>6 Write signatures for: filter, map, reduce, groupBy, distinct 7 Review: Read 10 signatures aloud. Identify which lack semantic naming. Week 2: Abstraction & Systems