Haskell Example:
taylor :: (Double -> Double) -> (Double -> Double) -> Double -> Double -> Double taylor f f' a x = f a + f' a * (x - a)
It was a joke!
If we were algebra professionals, we wouldn't write Go code! 😉
We like things simple, easy, and small.
This presentation is not about the mathematics in functional programming.
Our brain (our natural language model) has at least
a 6-year advantage in training natural language over math.
It's about how functional programming principles can help you write better code:
Functions with predictable outputs and no side effects.
Data cannot be modified after creation.
Functions treated as first-class citizens, can be passed and returned.
Functions are composable, allowing small, simple functions to combine into complex functionality.
A closure is a function that captures its scope, allowing access to variables after the scope exits.
Solving problems through recursive function calls instead of iterative loops.
Expressions can be replaced with their values without changing program behavior.
Powerful way to deconstruct and match data structures.
Writing flexible, reusable code that works with multiple types.
Composing complex types from simpler ones.
Data cannot be modified after creation.
Solving problems through recursive function calls instead of iterative loops.
Expressions can be replaced with their values without changing program behavior.
Powerful way to deconstruct and match data structures.
Composing complex types from simpler ones.
Haskell
let x = 5
let y = x + 3 -- x remains unchanged
Go
x := 5
x = x + 3 // x is modified
Definition: Same input, same output.
Benefit: Easier reasoning and testing.
Haskell
square x = x * x
square 5 -- Always evaluates to 25
Go
func square(x int) int {
fmt.Println("Squaring", x) // Side effect
return x * x
}
Definition: Simplifies control flow by destructuring and analyzing data.
Haskell
data Shape = Circle Float | Rectangle Float Float
area :: Shape -> Float
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
Go
type Shape interface{}
type Circle struct { Radius float64 }
type Rectangle struct { Width, Height float64 }
func area(s Shape) float64 {
switch shape := s.(type) {
case Circle:
return math.Pi * shape.Radius * shape.Radius
case Rectangle:
return shape.Width * shape.Height
default:
return 0
}
}
Everthing could be added here, no pattern matching, better use inteface with defined Area() function
Definition: Structuring data concisely using sum and product types.
Haskelldata Result = Success String | Failure String
Go
type Result struct {
Success bool
Message string
}
with manual effort to avoid side effects.
manual composition.
fully covered.
fully covered.
introduced in Go 1.18+, excluding methods.
f(x) → y // Always same output for same input
func square(x int) int { // A pure function in Go. It is deterministic and satisfies referential transparency.
return x * x
}
// For any x:
square(4) // Always returns 16
square(4) // Always returns 16
square(4) // Always returns 16
// But real world has side effects:
// - File operations might fail
// - Network calls can timeout
// - Values might be missing
// - Operations can fail
↓
function wrapValue(x) → box(x) // What if we could extend this?
Values with a context
Simple Value: 42
With Context: Result[42] // Success(42) or Error("failed")
Optional[42] // Some(42) or None()
In Go:
type Result[T any] struct {
value T
err error
}
// These containers let us:
func Transform[T, U any](r Result[T], f func(T) U) Result[U]
func Chain[T, U any](r Result[T], f func(T) Result[U]) Result[U]
These containers are called Monads.
Think of it as a container that follows three simple rules:
See? Monads aren't scary - they're just smart boxes!
This is how a typical function call looks like in Go:
value, err := doSomething()
if err != nil {
return err
}
// use value
Switch to the Step 1 branch for the monads.
Let's create our own Result monad.
What is this Monad about?
What has changed?
Not a big change yet?
What has changed here?
You think about results in your test description and no more about validations and assertions.
Isn't that simpler and easier?
What has changed here?
Asking the monad about its result context is more natural language.
Isn't that simpler and easier?
// multiple return values
GetOrder(ctx context.Context, orderID int) (*dsmodels.Order, error)
↓
// return single box/container --> Monad
GetOrder(ctx context.Context, orderID int) monads.Result[dsmodels.Order]
Only return a single entity
validateOrderMatches := func(t *testing.T, expected, actual *dsmodels.Order, err error) {
assert.NoError(t, err, "unexpected error received")
assert.Equal(t, expected, actual, "expected order mismatch")
}
validateOrderNotFound := func(t *testing.T, actual *dsmodels.Order, err error) {
assert.Nil(t, actual, "expected no order to be returned")
assert.EqualError(t, err, errOrderNotFound, "expected error mismatch")
}
validate: func(t *testing.T, order *dsmodels.Order, err error) {
validateOrderMatches(t, &dsmodels.Order{ID: 1, UserID: 123, Payments: []int{1, 2}}, order, err)
},
↓
//it makes our unit tests much simpler
expectedResult: monads.Ok(dsmodels.Order{ID: 1, UserID: 123, Payments: []int{1, 2}}),
It makes our unit tests much simpler.
//dsOrder, _ := service.storage.GetOrder(ctx, id)
dsOrder, err := service.storage.GetOrder(ctx, id)
↓
dsOrderResult := service.storage.GetOrder(ctx, id)
Clients can no longer ignore errors.
if err != nil
↓
if dsOrderResult.IsError()
Using a speaking method is closer to natural language for understanding what the code is about.
What has changed here?
How do we convert the result from the storage layer and transform and enrich it to the required result of the service?
What do we have here?
Wouldn't it be better if we had just a single return statement and a single check?
What did we say about a Monad: it's a value with context.
Let's use this knowledge about context.
What do we have here?
So what do we need to make use of it?
Let's change the functions to return Monads.
What do we have here?
How do we make use of these?
We now have 4 functions that all return a Result Monad.
What happened here?
What happened here?
// Authorization check
isAuthorized, err := service.authorizationService.IsAuthorized(ctx, userId, order)
if err != nil {
return nil, err
}
if !isAuthorized {
return nil, errors.New("user is not authorized to access this order")
}
// Add payments to the order
order, err = service.addPayments(ctx, order)
if err != nil {
return nil, err
}
// Add user to the order
order, err = service.addUser(ctx, order)
if err != nil {
return nil, err
}
return order, nil
// Authorization check
return service.verifyAuthorization(ctx, userId, order).
// Add payments to the order
FlatMap(func(value *models.Order) mo.Result[*models.Order] {
return service.addPayments(ctx, value)
}).
// Add user to the order
FlatMap(func(value *models.Order) mo.Result[*models.Order] {
return service.addUser(ctx, value)
})
Not readable like a book?
Imagine it like this:
// Authorization check
return service.verifyAuthorization(ctx, userId, order).
// Add payments to the order
FlatMap(func(value *models.Order) mo.Result[*models.Order] {
return service.addPayments(ctx, value)
}).
// Add user to the order
FlatMap(func(value *models.Order) mo.Result[*models.Order] {
return service.addUser(ctx, value)
})
// Authorization check
return service.verifyAuthorization(userId, order).
// Add payments to the order
FlatMap(service.addPayments).
// Add user to the order
FlatMap(service.addUser)
Better now?
But that's another story about how to remove the Context. ;)
This approach provided: