Learning Function Signatures in 3 Weeks/21 days

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

A function is not code. It is a contract.

f : A → B

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.


Day 2: Primitive Types

Before writing signatures, you must name your universe.

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 inhabitants

Exercise:

toString : Int → String
isPositive : Int → Bool
negate : Bool → Bool

Rule: A primitive type carries no semantic weight beyond its structure. Int tells you nothing about whether it represents an age, a count, or an ID.


Day 3: Composite Types — Products (Tuples & Records)

When a function needs multiple inputs or produces multiple outputs, you need product types.

Tuple notation (anonymous, positional):

divide : (Float, Float) → Float

Record notation (named, semantic):

divide : (dividend: Float, divisor: Float) → Float

The critical difference:

  • (Float, Float) says "two floats"
  • (dividend: Float, divisor: Float) says "a division operation"

Exercise:

createUser : (name: String, age: Int) → User
getCoordinates : () → (lat: Float, long: Float)

Day 4: Composite Types — Sums (Unions & Enums)

Not all outputs are singular. A function may return one of several types.

Status : Loading | Success<T> | Error<E>

Exercise:

parseInt : String → Int | ParseError
findUser : UserId → User | NotFound

Rule: Every union type is a decision point in your system. The consumer must handle every variant.


Day 5: Optional / Nullable Types

A special case of sum types: presence or absence.

Option<T> : Some<T> | None
Maybe<T>  : Just<T> | Nothing

Exercise:

findById : UserId → Option<User>
head : List<T> → Option<T>

Architectural insight: Option forces the caller to acknowledge absence. It removes the billion-dollar mistake from your contracts.


Day 6: Container Types — List, Map, Set

Functions rarely operate on single values. They operate on collections.

List<T>      : ordered, duplicate-permitted sequence
Set<T>       : unordered, duplicate-free collection  
Map<K, V>    : key-value association

Exercise:

filter : (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

Drill 1: Read this aloud.

transform : (A → B) → List<A> → List<B>

"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?

process : (String, Int) → String

Answer: The parameter names. We know the types but not the semantics.

Drill 3: Improve this signature.

f : (String, String) → Bool

Improved:

authenticate : (username: String, password: String) → AuthResult

Week 2: Abstraction & System Design

Day 8: Generic Types (Parametric Polymorphism)

Generics allow signatures to abstract over concrete types.

identity : T → T
pair : T → U → (T, U)
first : (T, U) → T

The Power: A generic signature makes a universal claim. identity : T → T states: "For all types T, this contract holds."

Exercise:

swap : (T, U) → (U, T)
compose : (B → C) → (A → B) → (A → C)

Day 9: Higher-Order Functions

Functions that accept or return other functions.

map : (T → U) → List<T> → List<U>
filter : (T → Bool) → List<T> → List<T>
reduce : (Acc → T → Acc) → Acc → List<T> → Acc

Reading complex signatures (right-associative):

curriedAdd : Int → Int → Int

This is: Int → (Int → Int) — a function that takes an Int and returns a function.

Exercise:

flip : (A → B → C) → (B → A → C)
on : (B → B → C) → (A → B) → (A → A → C)

Day 10: Domain-Specific Types (Newtypes)

Primitive types are structurally correct but semantically dangerous.

UserId  : newtype String
Email   : newtype String
Money   : newtype Int
Password: newtype String

Exercise:

findUser : UserId → Option<User>
sendEmail : Email → Body → Result<Email, SendError>
withdraw : AccountId → Money → Result<Balance, InsufficientFunds>

Rule: If two values should never be accidentally swapped, they must have distinct types.


Day 11: Effect Types — Result, IO, Option

Signatures must encode not just what is returned, but how the computation behaves.

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 computation

Exercise:

readFile : Path → IO<Result<String, FileError>>
fetchUser : UserId → Task<Result<User, NetworkError>>
parseConfig : String → Result<Config, ParseError>

Day 12: Type Constraints

Sometimes a generic type must support specific operations.

sort : Ord<T> => List<T> → List<T>
sum : Num<T> => List<T> → T
serialize : Json<T> => T → String

Reading: Ord<T> => means "This contract is valid for all T where an ordering relation exists."


Day 13: System Design — Authentication System

Design using only signatures. No database schemas. No flowcharts.

// 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>

System insight: Notice how 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 → MoneyAmount

Architectural boundary: Ledger 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>

Key decision: 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

Before:

process(data) // vague, untyped

After — Step 1 (Add types):

process : String → String

After — Step 2 (Add semantics):

process : JsonString → JsonString

After — Step 3 (Add domain types):

process : OrderPayload → Result<Confirmation, ValidationError>

Exercise: Refactor these:

doThing : (String, String) → String
handle : (Int, Int) → Bool
getStuff : String → List<String>

Solutions:

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

Systems are composed of function-shaped components.

Middleware pattern:

Middleware : (Request → Response) → (Request → Response)

Pipeline pattern:

Pipeline : List<(T → Result<T, E>)> → T → Result<T, List<E>>

Event handler pattern:

Handler<E> : E → List<Command>
Projection<S, E> : S → E → S

Day 19: Polymorphism & Typeclasses

Abstract over behavior, not just data.

// 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 pattern (dependency injection via types):

Reader<R, A> : R → A
getUser : Reader<UserRepository, User>

State pattern:

State<S, A> : S → (A, S)
increment : State<Counter, Unit>

Continuation pattern:

Cont<R, A> : (A → R) → R
withResource : Resource → (Resource → R) → R

Day 21: The Architect's Mindset

The Final Axioms

  1. Implementation is a detail. If the signature is correct, the implementation can be changed without breaking the system.

  2. Types are the only documentation that cannot lie. Comments become stale. Signatures are enforced.

  3. A complex signature indicates a complex contract. Do not simplify the signature to hide complexity. Make the complexity explicit so it can be managed.

  4. Side effects must be visible in types. A function f : A → B is pure. A function f : A → IO<B> is effectful. The difference is architectural.

  5. Compose signatures, not implementations. If f : A → B and g : B → C, then g ∘ f : A → C is guaranteed to connect.

The Design Checklist

Before accepting any signature, ask:

  • Are the input types precise enough to prevent invalid data?
  • Does the output type encode all possible outcomes (including failures)?
  • Are domain concepts represented as distinct types, not primitives?
  • Are effects (IO, async, failure) visible in the type?
  • Can this signature be composed with others without inspecting implementation?
  • Would this signature still make sense if the underlying technology changed?

Daily Practice Exercises (21 Days)

Week 1: Types & Notation

DayExercise
1Write signatures for: absolute value, string length, boolean negation
2Write signatures using: Int, Float, String, Bool, Char
3Convert these to record inputs: f(String, Int), g(Float, Float, Float)
4Write 5 functions that return union types
5Rewrite 3 functions from Day 4 using Option<T>
6Write signatures for: filter, map, reduce, groupBy, distinct
7Review: Read 10 signatures aloud. Identify which lack semantic naming.

Week 2: Abstraction & Systems

Week 3: Mastery & Evaluation

DayExercise
8Write generic signatures for: identity, constant, compose, pipe, flip
9Write higher-order signatures for: throttle, debounce, memoize, curry, uncurry
10Create 5 domain-specific newtypes and write signatures using them
11Rewrite Week 1 exercises using Result<T,E>, IO, Task
12Write constrained generics: sort (Ord), sum (Num), max (Ord), merge (Semigroup)
13Design complete Authentication system using only signatures
14Design complete Payment system using only signatures
DayExercise
15Design Chat system with real-time subscriptions
16Design File Upload system with chunked uploads
17Refactor 5 vague signatures into precise domain-specific contracts
18Write signatures for: Middleware, Pipeline, EventHandler, Projection
19Write signatures using Functor, Monad, Monoid constraints
20Write signatures for: Reader, State, Cont patterns
21Capstone: Design an E-commerce system (Catalog, Cart, Checkout, Inventory, Notification) using ONLY function signatures. No two functions may share the same input type unless explicitly generic.

Evaluation Rubric

When reviewing your signatures, I will assess:

CriterionPoorAcceptableExcellent
Type PrecisionUses only primitives (String, Int)Mixes primitives and some domain typesEvery concept has its own type
Failure EncodingReturns raw type (assumes success)Uses Option for absenceUses Result<T,E> with specific error types
Effect VisibilityNo effect typesUses IO inconsistentlyAll side effects explicit in types
NamingSingle letters or vague names (data, process)Descriptive but genericDomain-ubiquitous language (Invoice, Ledger)
ComposabilityFunctions share no type structureSome shared typesClear algebraic structure enabling composition
Constraint AppropriatenessNo constraintsOver-constrainedMinimal necessary constraints

Final Note

You are not learning to write functions. You are learning to design contracts between components.

When you can look at a blank page and populate it with nothing but precise, composable, effect-aware signatures that fully describe a banking platform, a real-time game server, or a distributed search engine — then you may begin to think about implementation.