Slide 1

Slide 1 text

RailsClub 2017 // @jodosha Functional Web with Hanami

Slide 2

Slide 2 text

Привет, Москва " MAXBORN

Slide 3

Slide 3 text

Luca Guidi https://lucaguidi.com @jodosha MAXBORN

Slide 4

Slide 4 text

Hanami http://hanamirb.org @hanamirb MAXBORN

Slide 5

Slide 5 text

MAXBORN https://dnsimple.com @dnsimple DNSimple

Slide 6

Slide 6 text

Stickers!!!

Slide 7

Slide 7 text

RailsClub 2017 // @jodosha Functional Web with Hanami

Slide 8

Slide 8 text

What is a function?

Slide 9

Slide 9 text

It’s an atomic set of instructions that (may) accept an input and (may) return an output.

Slide 10

Slide 10 text

A function is a task.

Slide 11

Slide 11 text

A function is the building block of our apps.

Slide 12

Slide 12 text

clear_database() # => nil Date.today() # => 2017-09-23 delete_file("foo.txt") # => nil twice(2) # => 4

Slide 13

Slide 13 text

What is a pure function?

Slide 14

Slide 14 text

It’s a function where the output is only determined by its input.

Slide 15

Slide 15 text

15 No side effects

Slide 16

Slide 16 text

16 Pop quiz

Slide 17

Slide 17 text

Pure or not? pure sum(2, 2)

Slide 18

Slide 18 text

Pure or not? not pure Time.now

Slide 19

Slide 19 text

Pure or not? pure "hanami".capitalize

Slide 20

Slide 20 text

Pure or not? not pure r = BookRepository.new r.create(title: "Hanami")

Slide 21

Slide 21 text

Pure or not? not pure c = ApiClient.new c.login("jodosha", "*******")

Slide 22

Slide 22 text

22 Pure functions have no side effects and they are deterministic.

Slide 23

Slide 23 text

$ shasum -a 256 file.txt 7fdd629e6366b7c758e1b823…

Slide 24

Slide 24 text

Pure or not? not pure def file_hash(path) Digest::SHA256.file(path).hexdigest end

Slide 25

Slide 25 text

Why is so much important to draw a line between pure and non-pure functions?

Slide 26

Slide 26 text

Because non-pure functions are unpredictable.

Slide 27

Slide 27 text

file_hash("testfile") # => Errno::ENOENT: # No such file or directory # @ rb_sysopen - testfile

Slide 28

Slide 28 text

Which kind of functions are easier to reason about and to test?

Slide 29

Slide 29 text

Pure!!!

Slide 30

Slide 30 text

it "returns 2 + 2" do expect(sum(2, 2)).to be(4) end Pure

Slide 31

Slide 31 text

Non-pure (1 of 4) before do touch(testfile) end let(:testfile) do "path/to/testfile" end

Slide 32

Slide 32 text

Non-pure (2 of 4) it "returns a hash for a given file" do # ... end

Slide 33

Slide 33 text

Non-pure (3 of 4) it "returns a different hash for another file" do append(testfile, "lorem ipsum..") # ... end

Slide 34

Slide 34 text

Non-pure (4 of 4) it “raises an error when file is missing" do delete(testfile) # ... end

Slide 35

Slide 35 text

before do touch(testfile) end let(:testfile) { "path/to/testfile" } it "returns a hash for a given file" do # ... end it "returns a different hash for another file” do append(testfile, "lorem ipsum..") # ... end it "raises an error when file is missing" do delete(testfile) # ... end Pure vs non-pure it "returns 2 + 2" do expect(sum(2, 2)).to be(4) end

Slide 36

Slide 36 text

36 Pure functions require less setup and less specs.

Slide 37

Slide 37 text

37 Non-pure functions are harder to test.

Slide 38

Slide 38 text

38

Slide 39

Slide 39 text

Which kind of functions would you like to see more in your apps? Pure or non- pure?

Slide 40

Slide 40 text

Pure!!!

Slide 41

Slide 41 text

String#capitalize string = "hello world" string.capitalize # => "Hello world" string.capitalize # => "Hello world" string.capitalize # => "Hello world"

Slide 42

Slide 42 text

Deterministic (1 of 2) class Formatter def initialize(string) @string = string end def capitalize @string.capitalize end end

Slide 43

Slide 43 text

Deterministic (2 of 2) formatter = Formatter.new("hello world") formatter.capitalize # => "Hello world" formatter.capitalize # => "Hello world" formatter.capitalize # => "Hello world"

Slide 44

Slide 44 text

Non-deterministic (1 of 2) class Formatter # ... def replace(other) @string.replace(other) end end

Slide 45

Slide 45 text

Non-deterministic (2 of 2) formatter = Formatter.new("hello world") formatter.capitalize # => "Hello world" formatter.replace("foo") formatter.capitalize # => "Foo" ⚠⚠⚠

Slide 46

Slide 46 text

46 Mutable state breaks the deterministic nature of our functions.

Slide 47

Slide 47 text

47

Slide 48

Slide 48 text

class User < ORM def initialize(attributes) @attributes = attributes end def capitalize @attributes.fetch(:name).capitalize end def update_attributes(other) @attributes.merge!(other) end end

Slide 49

Slide 49 text

Hanami (1 of 3) class User < Hanami::Entity def capitalize name.capitalize end end repository = UserRepository.new

Slide 50

Slide 50 text

Hanami (2 of 3) # create user = repository.create(params[:user]) user.id # => 1 user.capitalize # => "Luca" user.capitalize # => "Luca" user.capitalize # => "Luca"

Slide 51

Slide 51 text

Hanami (3 of 3) # update user = repository.update( params[:id], params[:user]) user.id # => 1 user.capitalize # => “Luca Guidi" user.capitalize # => "Luca Guidi" user.capitalize # => "Luca Guidi”

Slide 52

Slide 52 text

Recap * Pure functions * Immutability

Slide 53

Slide 53 text

53

Slide 54

Slide 54 text

A function is the building block of our apps.

Slide 55

Slide 55 text

55 The most important feature for Lego bricks is compatibility.

Slide 56

Slide 56 text

clear_database() # => nil Date.today() # => 2017-09-23 delete_file("foo.txt") # => nil twice(2) # => 4

Slide 57

Slide 57 text

def add(a, b) a + b end ->(a, b) { a + b } Sum.new Sum

Slide 58

Slide 58 text

58 Callable Objects

Slide 59

Slide 59 text

->(a, b) { a + b }.call(2, 2) # => 4 method(:add).call(2, 2) # => 4 Sum.new.call(2, 2) # => 4 Sum.call(2, 2) # => 4

Slide 60

Slide 60 text

->(a, b) { a + b }.call(2, 2) # => 4 method(:add) .call(2, 2) # => 4 Sum.new .call(2, 2) # => 4 Sum .call(2, 2) # => 4

Slide 61

Slide 61 text

["hello", "world"].map(&:upcase) # => [“HELLO", “WORLD"]

Slide 62

Slide 62 text

sum = ->(a, b) { a + b } sum.call(1, 2) # => 3 (1..20).inject(&sum) # => 210

Slide 63

Slide 63 text

multi = ->(a, b) { a * b } multi.(2, 3) # => 6 pow = ->(a, n) do n.times.inject(1) do |memo| memo = multi.(memo, a) end end pow.(2, 5) # => 32 pow.(2, 0) # => 1 # quadratic equations...

Slide 64

Slide 64 text

64 We can make our logic (objects) to become “higher order functions”.

Slide 65

Slide 65 text

class CompareNumbers def call(a, b) a <=> b end def to_proc method(:call).to_proc end end CompareNumbers.new.call(23, 11) # => 1 [23, 11, 99].sort(&CompareNumbers.new) # => [11, 23, 99]

Slide 66

Slide 66 text

66

Slide 67

Slide 67 text

67 Compatibility Reusability Small and focused objects

Slide 68

Slide 68 text

Recap * Pure functions * Immutability * Callable Objects

Slide 69

Slide 69 text

69 The big picture

Slide 70

Slide 70 text

What is an application?

Slide 71

Slide 71 text

It’s a set of features.

Slide 72

Slide 72 text

What is a feature?

Slide 73

Slide 73 text

A feature it’s a task for our users.

Slide 74

Slide 74 text

A function is a task.

Slide 75

Slide 75 text

A feature is a big function made of smaller functions.

Slide 76

Slide 76 text

How to structure a feature?

Slide 77

Slide 77 text

MVC?

Slide 78

Slide 78 text

78 V C MVC

Slide 79

Slide 79 text

79 Identify and structure common tasks.

Slide 80

Slide 80 text

What are the common tasks for a feature?

Slide 81

Slide 81 text

1. Route the request 2. Verify the request (verb, MIME) 3. Authentication 4. Coerce & validate input 5. Transform input 6. Verify preconditions (rules) 7. CRUD 8. Trigger postconditions 9. Present the output

Slide 82

Slide 82 text

82 Task Component Route the request Hanami::Router HTTP verb/MIME Hanami::Action Authentication (custom) Coerce & validate Hanami::Validations Transform (missing) Preconditions (custom) CRUD Hanami::Repository Postconditions (missing) Present Hanami::View

Slide 83

Slide 83 text

83 Task Component Func? Route the request Hanami::Router ✅ yes HTTP verb/MIME Hanami::Action ❌ no Authentication (custom) ❗not enforced Coerce & validate Hanami::Validations ❌ no Transform Hanami::Transformer in progress Preconditions (custom) ❗not enforced CRUD Hanami::Repository ✅ yes Postconditions Hanami::Events in progress Present Hanami::View ❌ no

Slide 84

Slide 84 text

84

Slide 85

Slide 85 text

Current design class MyValidator include Hanami::Validations validations do required(:name).filled(:str?) end end result = MyValidator.new(name: "Luca").validate result.successful? # => true

Slide 86

Slide 86 text

Future design class MyValidator < Hanami::Validator validations do required(:name).filled(:str?) end end result = MyValidator.new.call(name: "Luca") result.successful? # => true

Slide 87

Slide 87 text

Self-registering (1 of 3) UserRepository.new => "users" WelcomeMailer.new => "mailers.welcome" SignupValidator.new => "validators.signup"

Slide 88

Slide 88 text

Self-registering (2 of 3) module Web::Controllers::Users class Create include Web::Action def call(params) UserRepository.new.create(params[:user]) end end end

Slide 89

Slide 89 text

Self-registering (3 of 3) module Web::Controllers::Users class Create < Hanami::Action import :users def call(params) users.create(params[:user]) end end end

Slide 90

Slide 90 text

90 Self-registration is possible via a fine tuned mechanism, which is able to resolve a tree of dependencies and to inject them.

Slide 91

Slide 91 text

Recap * Pure functions * Immutability * Callable Objects * Dependency Injection

Slide 92

Slide 92 text

92 Task Pure? Route the request ✅ yes HTTP verb/MIME ✅ yes Authentication ✅ yes Coerce & validate ✅ yes Transform ✅ yes Preconditions ✅ yes CRUD ❌ no* Postconditions ❌ no* Present ✅ yes

Slide 93

Slide 93 text

93 Task Pure? MVC Route the request ✅ yes Controller HTTP verb/MIME ✅ yes Controller Authentication ✅ yes Controller Coerce & validate ✅ yes Model Transform ✅ yes Model Preconditions ✅ yes Model CRUD ❌ no* Model Postconditions ❌ no* Model Present ✅ yes View

Slide 94

Slide 94 text

94 V C MVC

Slide 95

Slide 95 text

How to structure a feature?

Slide 96

Slide 96 text

Pipeline

Slide 97

Slide 97 text

97 A pipeline is a set of compatible components that are executed in a specific order. If one of those steps returns an error, the execution is stopped.

Slide 98

Slide 98 text

1. Route the request 2. Verify the request (verb, MIME) 3. Authentication 4. Coerce & validate input 5. Transform input 6. Verify preconditions (rules) 7. CRUD 8. Trigger postconditions 9. Present the output

Slide 99

Slide 99 text

Pipeline (1 of 2) class Signup < Hanami::Interactor import :validator, :transformer, :users pipeline :validator, :transformer, :persist, :notify private def persist(input:, output:) output[:user] = users.create(input) end def notify(output:, **) events.broadcast(“signup", user_id: output[:user].id) end end

Slide 100

Slide 100 text

Pipeline (2 of 2) module Web::Controllers::Users class Create < Hanami::Action import :signup def call(params) result = signup.call(params[:signup]) if result.successful? # ... else # ... end end end end

Slide 101

Slide 101 text

101

Slide 102

Slide 102 text

Recap * Pure functions * Immutability * Callable Objects * Dependency Injection * Pipelines

Slide 103

Slide 103 text

Спасибо! https://lucaguidi.com @jodosha MAXBORN