A presentation by @stuherbert
for @GanbaroDigital
Railway-Oriented
Programming
A Functional Approach In PHP
Slide 2
Slide 2 text
Industry veteran: architect, engineer,
leader, manager, mentor
F/OSS contributor since 1994
Talking and writing about PHP
since 2004
Chief Software Archaeologist
Building Quality @GanbaroDigital
About Stuart
Slide 3
Slide 3 text
Follow me
I do tweet a lot about
non-tech stuff though :)
@stuherbert
Slide 4
Slide 4 text
@GanbaroDigital
I am not a
functional programmer.
Slide 5
Slide 5 text
@GanbaroDigital
https://flic.kr/p/bWVcA1
I’m On A Quest
Slide 6
Slide 6 text
@GanbaroDigital
My interest is
making code
more reusable.
Slide 7
Slide 7 text
@GanbaroDigital
My second interest
is making code
easier to reason about.
Slide 8
Slide 8 text
@GanbaroDigital
My third interest is
making code
more robust.
Slide 9
Slide 9 text
@GanbaroDigital
RRR Development
• Reusable
• Reason about it
• Robust
... without sacrificing performance!
Slide 10
Slide 10 text
@GanbaroDigital
https://flic.kr/p/iH2v8F
Am I Tilting
At Windmills?
Slide 11
Slide 11 text
@GanbaroDigital
Why Look At
Functional
Programming?
Slide 12
Slide 12 text
@GanbaroDigital
“
PHP is an imperative language ...
... with a Java-ish OOP identity.
Slide 13
Slide 13 text
@GanbaroDigital
“ Reuse in OO
is, always has been,
and always will be
disappointing.
Slide 14
Slide 14 text
@GanbaroDigital
Reuse Challenges
• Side effects
• High coupling
• Bitrot / fragility to change
Slide 15
Slide 15 text
@GanbaroDigital
Classes and objects
normally*
wrap state in behaviour
Slide 16
Slide 16 text
@GanbaroDigital
Object
Slide 17
Slide 17 text
@GanbaroDigital
Object
Data
Slide 18
Slide 18 text
@GanbaroDigital
Object
Data
Methods
Slide 19
Slide 19 text
@GanbaroDigital
Object
Data
Methods
Slide 20
Slide 20 text
@GanbaroDigital
Object
Data
Methods
Slide 21
Slide 21 text
@GanbaroDigital
Object
Data
Methods
Slide 22
Slide 22 text
@GanbaroDigital
That’s not
the whole picture.
Slide 23
Slide 23 text
@GanbaroDigital
Object
Data
Methods
Slide 24
Slide 24 text
@GanbaroDigital
Object
Slide 25
Slide 25 text
@GanbaroDigital
Object
Slide 26
Slide 26 text
@GanbaroDigital
Object
Slide 27
Slide 27 text
@GanbaroDigital
Object
Slide 28
Slide 28 text
@GanbaroDigital
Object
Data
Methods
Slide 29
Slide 29 text
@GanbaroDigital
What We Can See
What We Can’t See
Slide 30
Slide 30 text
@GanbaroDigital
What We Can See
What We Can’t See
Slide 31
Slide 31 text
@GanbaroDigital
What We Can See
What We Can’t See
Slide 32
Slide 32 text
@GanbaroDigital
“
When there’s high-coupling,
there’s high sensitivity to change.
Slide 33
Slide 33 text
@GanbaroDigital
“A change in one of the things
we can’t see
can break the one thing
we can see.
Slide 34
Slide 34 text
@GanbaroDigital
??
??
Do we notice the breakage
a) at all?
b) before we ship?
Slide 35
Slide 35 text
@GanbaroDigital
??
??
How long does it take
to find the cause
of the breakage?
Slide 36
Slide 36 text
@GanbaroDigital
“
It’s the nature of OOP
to create highly-coupled code.
Slide 37
Slide 37 text
@GanbaroDigital
“
Breaking large objects / methods
into smaller ones
does nothing to reduce coupling.
Slide 38
Slide 38 text
@GanbaroDigital
That’s why I’m interested
in other programming paradigms
to help me on my quest.
Slide 39
Slide 39 text
@GanbaroDigital
Your quest will be different,
and just as valid.
I’m here to learn from you too!
Slide 40
Slide 40 text
@GanbaroDigital
Railway-Oriented
Programming
Slide 41
Slide 41 text
@GanbaroDigital
Scott Wlaschin
https://fsharpforfunandprofit.com/
Slide 42
Slide 42 text
@GanbaroDigital
https://vimeo.com/113707214
Slide 43
Slide 43 text
@GanbaroDigital
What Is
Railway-Oriented
Programming?
Slide 44
Slide 44 text
@GanbaroDigital
Slide 45
Slide 45 text
@GanbaroDigital
input
Slide 46
Slide 46 text
@GanbaroDigital
input output
Slide 47
Slide 47 text
@GanbaroDigital
“The Tunnel
of Transformation”
input output
Slide 48
Slide 48 text
@GanbaroDigital
strtoupper()
Slide 49
Slide 49 text
@GanbaroDigital
strtoupper()
Slide 50
Slide 50 text
@GanbaroDigital
strtoupper()
string
Slide 51
Slide 51 text
@GanbaroDigital
strtoupper()
string string
Slide 52
Slide 52 text
@GanbaroDigital
strtoupper()
apple
Slide 53
Slide 53 text
@GanbaroDigital
strtoupper()
apple APPLE
Slide 54
Slide 54 text
@GanbaroDigital
“ One thing goes in,
a new thing comes out.
The thing that went in
remains unchanged.
Slide 55
Slide 55 text
@GanbaroDigital
Slide 56
Slide 56 text
@GanbaroDigital
Slide 57
Slide 57 text
@GanbaroDigital
Slide 58
Slide 58 text
@GanbaroDigital
Slide 59
Slide 59 text
@GanbaroDigital
{
Composability
Slide 60
Slide 60 text
@GanbaroDigital
Scott’s interest in ROP
started from error handling.
Slide 61
Slide 61 text
@GanbaroDigital
??
??
What happens
when things go wrong?
@GanbaroDigital
We could throw
an Exception instead.
Slide 65
Slide 65 text
@GanbaroDigital
https://flic.kr/p/fArumY
Pyramid
of Dooom!
Slide 66
Slide 66 text
@GanbaroDigital
public function validateEmail($email)
{
if (preg_match(..., $email)) {
if (not_blacklisted($email)) {
if (mailbox_exists($email)) {
return $email;
}
}
}
throw new Exception(...);
}
Slide 67
Slide 67 text
@GanbaroDigital
public function validateEmail($email)
{
if (preg_match(..., $email)) {
if (not_blacklisted($email)) {
if (mailbox_exists($email)) {
return $email;
}
}
}
throw new Exception(...);
}
Slide 68
Slide 68 text
@GanbaroDigital
All Joking Aside ...
• Nested ‘if’ adds testing complexity
• Becomes fragile over time, as rules change
Slide 69
Slide 69 text
@GanbaroDigital
All Joking Aside ...
• Nested ‘if’ adds testing complexity
• Becomes fragile over time, as rules change
Slide 70
Slide 70 text
@GanbaroDigital
Can we replace
the Pyramid of Doom
with ROP?
Slide 71
Slide 71 text
@GanbaroDigital
public function validateEmail($email)
{
if (preg_match(..., $email)) {
if (not_blacklisted($email)) {
if (mailbox_exists($email)) {
return $email;
}
}
}
throw new Exception(...);
}
Slide 72
Slide 72 text
@GanbaroDigital
public function validateEmail($email)
{
if (preg_match(..., $email)) {
if (not_blacklisted($email)) {
if (mailbox_exists($email)) {
return $email;
}
}
}
throw new Exception(...);
}
@GanbaroDigital
ROP Function Actions
• Apply logic to input data
• Produce new output data
• Input data remains unchanged
Slide 94
Slide 94 text
@GanbaroDigital
ROP Function Outputs
• The (possibly transformed) data
• (Aggregated) error reporting
Slide 95
Slide 95 text
@GanbaroDigital
ROP Function Composition
• Maybe we want to short-circuit on error
• Maybe we want to aggregate errors
• This is policy - it belongs in the calling code
Slide 96
Slide 96 text
@GanbaroDigital
??
??
What would ROP
look like in PHP?
Slide 97
Slide 97 text
@GanbaroDigital
PHP is an imperative language.
It lacks things that
functional programmers
take for granted.
Slide 98
Slide 98 text
@GanbaroDigital
FL Advantages
• Type system
• Compile-time checks*
• Monads & monoids
Slide 99
Slide 99 text
@GanbaroDigital
??
??
Should we simply
emulate monads in PHP?
Slide 100
Slide 100 text
@GanbaroDigital
We can emulate
the flow logic of Monads.
We can’t emulate
the type system
that makes them practical.
Slide 101
Slide 101 text
@GanbaroDigital
Let’s revisit this question
after the code demos.
Slide 102
Slide 102 text
@GanbaroDigital
ROP in PHP
Slide 103
Slide 103 text
@GanbaroDigital
Starting Requirements
• Data input, optional error input
• Data output, error output
• Input data treated as immutable
Slide 104
Slide 104 text
@GanbaroDigital
Design Constraints
• Functions must be composable
• Short-circuit logic sits outside composed
functions
@GanbaroDigital
“ Standardisation
encourages
interoperability
and reusability.
Slide 131
Slide 131 text
@GanbaroDigital
“
A standardised approach
allows us to reuse logic
to create new logic.
Slide 132
Slide 132 text
@GanbaroDigital
Slide 133
Slide 133 text
@GanbaroDigital
Slide 134
Slide 134 text
@GanbaroDigital
Reasonability:
Reusability:
Robustness:
✓
½
Slide 135
Slide 135 text
@GanbaroDigital
Some Observations
• Failure is currently a placeholder
• Return values aren’t typed
Slide 136
Slide 136 text
@GanbaroDigital
We’ll come back to
handling failure shortly.
Slide 137
Slide 137 text
@GanbaroDigital
Some Observations
• Failure is currently a placeholder
• Return values aren’t typed
Slide 138
Slide 138 text
@GanbaroDigital
??
??
How do we feel
about the return types?
Slide 139
Slide 139 text
@GanbaroDigital
Slide 140
Slide 140 text
@GanbaroDigital
Example 2:
Business Domain
Slide 141
Slide 141 text
@GanbaroDigital
Let’s model
a very simple*
e-commerce order.
Slide 142
Slide 142 text
@GanbaroDigital
Slide 143
Slide 143 text
@GanbaroDigital
Slide 144
Slide 144 text
@GanbaroDigital
Slide 145
Slide 145 text
@GanbaroDigital
Slide 146
Slide 146 text
@GanbaroDigital
Slide 147
Slide 147 text
@GanbaroDigital
Slide 148
Slide 148 text
@GanbaroDigital
Slide 149
Slide 149 text
@GanbaroDigital
“
The business model
is logic that is applied
to the data model.
Slide 150
Slide 150 text
@GanbaroDigital
“Separate the business model
from the data model
for flexibility
and long-term stability.
Slide 151
Slide 151 text
@GanbaroDigital
Slide 152
Slide 152 text
@GanbaroDigital
Slide 153
Slide 153 text
@GanbaroDigital
Both of our example functions
return new Orders.
The original Order
is left unchanged.
Slide 154
Slide 154 text
@GanbaroDigital
If anything goes wrong*,
we still have
the original Order.
* and it will :-)
Slide 155
Slide 155 text
@GanbaroDigital
??
??
How do we make
these functions
composable?
Slide 156
Slide 156 text
@GanbaroDigital
Slide 157
Slide 157 text
@GanbaroDigital
Slide 158
Slide 158 text
@GanbaroDigital
Slide 159
Slide 159 text
@GanbaroDigital
Having standalone functions
that you then wrap
can be easier to unit test.
Slide 160
Slide 160 text
@GanbaroDigital
Slide 161
Slide 161 text
@GanbaroDigital
Slide 162
Slide 162 text
@GanbaroDigital
Use a function that returns a function
to make things composable
AND
avoid hard-coding parameters.
Slide 163
Slide 163 text
@GanbaroDigital
Slide 164
Slide 164 text
@GanbaroDigital
Slide 165
Slide 165 text
@GanbaroDigital
Slide 166
Slide 166 text
@GanbaroDigital
Slide 167
Slide 167 text
@GanbaroDigital
??
??
How do you feel
about lambda functions
in PHP?
Slide 168
Slide 168 text
@GanbaroDigital
Rule of thumb:
any function that takes 1 parameter
and isn’t built from composed functions
assume it has something
hard-coded in there
until you prove otherwise!
Slide 169
Slide 169 text
@GanbaroDigital
Slide 170
Slide 170 text
@GanbaroDigital
Slide 171
Slide 171 text
@GanbaroDigital
Slide 172
Slide 172 text
@GanbaroDigital
Slide 173
Slide 173 text
@GanbaroDigital
Slide 174
Slide 174 text
@GanbaroDigital
Slide 175
Slide 175 text
@GanbaroDigital
Slide 176
Slide 176 text
@GanbaroDigital
Slide 177
Slide 177 text
@GanbaroDigital
With the ROP approach,
you might end up
building a lot of lambda functions.
Slide 178
Slide 178 text
@GanbaroDigital
These builders are creating
partial functions.
Slide 179
Slide 179 text
@GanbaroDigital
We can create a reusable
partial function builder.
Slide 180
Slide 180 text
@GanbaroDigital
Slide 181
Slide 181 text
@GanbaroDigital
Partial Function Builder
• Reduces amount of code to write
• Requires main data to be first parameter of
the wrapped function
• Convenience over runtime performance
• No type-safety
Slide 182
Slide 182 text
@GanbaroDigital
Partial Function Builder
• Reduces amount of code to write
• Requires main data to be first parameter of
the wrapped function
• Convenience over runtime performance
• No type-safety
Slide 183
Slide 183 text
@GanbaroDigital
Partial Function Builder
• Reduces amount of code to write
• Requires main data to be first parameter of
the wrapped function
• Convenience over runtime performance
• No type-safety
Slide 184
Slide 184 text
@GanbaroDigital
Partial Function Builder
• Reduces amount of code to write
• Requires main data to be first parameter of
the wrapped function
• Convenience over runtime performance
• No type-safety
Slide 185
Slide 185 text
@GanbaroDigital
“
A standardised business domain
can adapt to change.
Slide 186
Slide 186 text
@GanbaroDigital
Let’s add discount codes
to our worked example.
Slide 187
Slide 187 text
@GanbaroDigital
Requirements
• Applied after all other costs
• Show the discount to make the customer feel
the value
Slide 188
Slide 188 text
@GanbaroDigital
Slide 189
Slide 189 text
@GanbaroDigital
Requirements
• Applied after all other costs
• Show the discount to make the customer feel
the value
Slide 190
Slide 190 text
@GanbaroDigital
Slide 191
Slide 191 text
@GanbaroDigital
Slide 192
Slide 192 text
@GanbaroDigital
A purely imperative approach
is readable
but has a larger change surface.
Slide 193
Slide 193 text
@GanbaroDigital
Slide 194
Slide 194 text
@GanbaroDigital
A composable / ROP approach
has a smaller change surface
but requires more mental space.
Slide 195
Slide 195 text
@GanbaroDigital
Slide 196
Slide 196 text
@GanbaroDigital
Introducing discounts hasn’t touched
our gross calculation at all!
Slide 197
Slide 197 text
@GanbaroDigital
The composable / ROP approach
is very suited to
switching business logic
at runtime.
Slide 198
Slide 198 text
@GanbaroDigital
It’s one way to adopt the
DDD “specifications” concept.
@GanbaroDigital
Scott talked about ROP
as an approach to error handling.
We can’t put it off
any longer :-)
Slide 201
Slide 201 text
@GanbaroDigital
Example 3:
Failure
Slide 202
Slide 202 text
@GanbaroDigital
Slide 203
Slide 203 text
@GanbaroDigital
Slide 204
Slide 204 text
@GanbaroDigital
Slide 205
Slide 205 text
@GanbaroDigital
Slide 206
Slide 206 text
@GanbaroDigital
Slide 207
Slide 207 text
@GanbaroDigital
Slide 208
Slide 208 text
@GanbaroDigital
??
??
How can we handle
the missing VAT code?
Slide 209
Slide 209 text
@GanbaroDigital
Slide 210
Slide 210 text
@GanbaroDigital
get_vat_rate()
silently hides
an error from elsewhere
Slide 211
Slide 211 text
@GanbaroDigital
get_vat_rate()
is propagating an error
that we cannot see
Slide 212
Slide 212 text
@GanbaroDigital
??
??
How can we handle
the missing VAT code?
Slide 213
Slide 213 text
@GanbaroDigital
Option 1:
throw an exception
Slide 214
Slide 214 text
@GanbaroDigital
Slide 215
Slide 215 text
@GanbaroDigital
Slide 216
Slide 216 text
@GanbaroDigital
InvalidArgumentException
is one of those things
I wish we could uninvent.
Slide 217
Slide 217 text
@GanbaroDigital
Option 1 Consequences
• We don’t need $failure at all (not ROP!)
• Generic exceptions are a huge time sink
when investigating faults
• Specific exceptions lead to large try/catch
blocks*
• try/catch blocks are part of our change
surface area - Pryamid of Doom / fragile
Slide 218
Slide 218 text
@GanbaroDigital
Option 1 Consequences
• We don’t need $failure at all (not ROP!)
• Generic exceptions are a huge time sink
when investigating faults
• Specific exceptions lead to large try/catch
blocks*
• try/catch blocks are part of our change
surface area - Pryamid of Doom / fragile
Slide 219
Slide 219 text
@GanbaroDigital
Option 1 Consequences
• We don’t need $failure at all (not ROP!)
• Generic exceptions are a huge time sink
when investigating faults
• Specific exceptions lead to large try/catch
blocks*
• try/catch blocks are part of our change
surface area - Pryamid of Doom / fragile
Slide 220
Slide 220 text
@GanbaroDigital
Option 1 Consequences
• We don’t need $failure at all (not ROP!)
• Generic exceptions are a huge time sink
when investigating faults
• Specific exceptions lead to large try/catch
blocks*
• try/catch blocks are part of our change
surface area - Pryamid of Doom / fragile
Slide 221
Slide 221 text
@GanbaroDigital
Option 2:
return the error
as $failure
@GanbaroDigital
Option 2 Consequences
• Everything has to agree on what $failure is,
and how to use it
• Still need to return *something* as main data
out
• Someone needs to remember to check
$failure
Slide 227
Slide 227 text
@GanbaroDigital
Option 2 Consequences
• Everything has to agree on what $failure is,
and how to use it
• Still need to return *something* as main data
out
• Someone needs to remember to check
$failure
Slide 228
Slide 228 text
@GanbaroDigital
Option 2 Consequences
• Everything has to agree on what $failure is,
and how to use it
• Still need to return *something* as main data
out
• Someone needs to remember to check
$failure
Slide 229
Slide 229 text
@GanbaroDigital
Option 1 is effort and fragile.
(And it’s the current way)
Option 2 requires
100% accuracy from humans.
Slide 230
Slide 230 text
@GanbaroDigital
??
??
What if the caller
tells us how to handle failure?
@GanbaroDigital
input output
error
handler
error
handler
“The Tunnel
of Transformation”
Slide 240
Slide 240 text
@GanbaroDigital
Option 3 Consequences
• Everything still has to agree on what $failure
is, and how to use it
• 2nd parameter is non-optional callback
• Caller decides how to handle errors
• Caller can throw exceptions if preferred
Slide 241
Slide 241 text
@GanbaroDigital
Option 3 Consequences
• Everything still has to agree on what $failure
is, and how to use it
• 2nd parameter is non-optional callback
• Caller decides how to handle errors
• Caller can throw exceptions if preferred
Slide 242
Slide 242 text
@GanbaroDigital
Option 3 Consequences
• Everything still has to agree on what $failure
is, and how to use it
• 2nd parameter is non-optional callback
• Caller decides how to handle errors
• Caller can throw exceptions if preferred
Slide 243
Slide 243 text
@GanbaroDigital
Option 3 Consequences
• Everything still has to agree on what $failure
is, and how to use it
• 2nd parameter is non-optional callback
• Caller decides how to handle errors
• Caller can throw exceptions if preferred
Slide 244
Slide 244 text
@GanbaroDigital
Our partial function builder
now knows what to expect
for the second parameter.