70
TAKING INSPIRATION FROM THE FUNCTIONAL WORLD BY PIOTR SOLNICA // LAMBDA WORLD

Taking Inspiration From The Functional World

Embed Size (px)

Citation preview

TAKING INSPIRATION FROM THE FUNCTIONAL WORLD

BY PIOTR SOLNICA // LAMBDA WORLD

What am I doing here?

Disappointed with OO¯\_(⊙_ʖ⊙)_/¯

I’m a rubyist(practically speaking)

I’ve fallen in <3 with FP

How did that happen?

~3 years of OO during studies

~10 years of OO at work

Clean Code

Patterns, rules, principles, best practices, code smells, metric tools, refactoring, TDD, mutation testing, …

༼ ˵ ʖ͟ ˵༽

What am I doing?

FP => light at the end of the tunnel(I hope it’s not a train c(ˊᗜˋ*c))

FP-INSPIRED RUBY

MUTABLE STATE :(

arr = ["foo", "bar"]

arr.freeze

arr << "baz"

# `<main>': can't modify frozen Array (RuntimeError)

arr[0].upcase!

arr

# ["FOO", "bar"]

require 'ice_nine'

arr = ["foo", "bar"]

IceNine.deep_freeze(arr)

arr[0].upcase!

# `upcase!': can't modify frozen String

# (RuntimeError)

What about my objects?

class Post attr_reader :tags

def initialize(tags) @tags = tags endend

post = Post.new(["red", "green", "blue"])

post.tags[0].upcase!

post

#<Post:0x007fc69c1785d0 @tags=["RED", "green",

"blue"]>

require 'adamantium'

class FrozenPost include Adamantium

attr_reader :tags

def initialize(tags) @tags = tags endend

post = FrozenPost.new(["red", "green", "blue"])

post.tags[0].upcase!

# `upcase!': can't modify frozen String (RuntimeError)

class MutablePost attr_reader :tags

def initialize(tags) @tags = tags endend

class FrozenPost include Adamantium

attr_reader :tags

def initialize(tags) @tags = tags endend

Benchmark.ips do |x| x.report('MutablePost') { MutablePost.new(["red", "green", “blue"]) }

x.report('FrozenPost') { FrozenPost.new(["red", "green", “blue"]) }

x.compare!end

# Calculating -------------------------------------# MutablePost 81.643k i/100ms# FrozenPost 12.311k i/100ms# -------------------------------------------------# MutablePost 1.850M (± 5.3%) i/s - 9.226M# FrozenPost 139.753k (± 4.0%) i/s - 701.727k## Comparison:# MutablePost: 1849628.0 i/s# FrozenPost: 139752.8 i/s - 13.23x slower

So, how is that useful?

For eduction purposes(mostly)

TRANSFORMING DATA

Enumerable

arr = [ { name: 'Joe', age: 21 }, { name: 'Jane', age: 20 }]

arr.map do |user| { user_name: user[:name], user_age: user[:age] }end# [{:user_name=>"Joe", :user_age=>21}, # :user_name=>”Jane", :user_age=>20}]

BlockReceives a hash

Returns a new hash

How can we reuse the block?

arr.map(&prefix)# [{:user_name=>"Joe", :user_age=>21},# {:user_name=>"Jane", :user_age=>20}]

prefix = -> user do { user_name: user[:name], user_age: user[:age] }end

lambda

pass lambda as block

arr.map(&prefix)

array object

method

lambda

{?

Monkey-patching :(

arr.pluck(:age)# [21, 20]

module Enumerable def pluck(name) map { |item| item[name] } endend

Method calls are chainable

arr.pluck(:name).map(&:upcase)# ["JOE", "JANE"]

…but they are not composable

Method Objects

to the rescue

pluck_age = -> arr { arr.map { |user| user[:age] }}

pluck_age.call(arr)# [21, 20]

pluck_age = Mappings.method(:pluck_age)# #<Method: Mappings.pluck_age>

module Mappings def self.pluck_age(arr) arr.map { |user| user[:age] } endend

pluck_age.call(arr)

How do we compose them?

require 'transproc'

module Mappings extend Transproc::Registry

def self.pluck_age(arr) arr.map { |user| user[:age] } endend

t = Mappings

t[:pluck_age].call(arr)# [21, 20]

#[] returns a callable function

module Mappings extend Transproc::Registry

def self.pluck(hash, key) hash[key] endend

t[:pluck, :age].call(name: 'Jane', age: 20)# 20

module Mappings extend Transproc::Registry

def self.pluck(hash, key) hash[key] end

def self.upcase(input) input.upcase endend

transformation = t[:pluck, :name] >> t[:upcase]

transformation.call(name: 'Jane', age: 20)# "JANE"

Pipeline composition operator

Importing other modules

require 'transproc/all'require 'inflecto'

module Mappings extend Transproc::Registry

import :extract_key, from: Transproc::ArrayTransformations

import :underscore, from: Inflectoend

External library

Transproc’s builtin transformations

transformation.call( [{ field: ‘UserName' }, { field: 'UserAddress' }])# ["user_name", "user_address"]

transformation = t[:extract_key, :field] >> t[:underscore]

MONADS

…SOME RUBY GEMS IMPLEMENTING VARIOUS MONADS

▸ https://github.com/tomstuart/monads

▸ https://github.com/pzol/monadic

▸ https://github.com/pzol/deterministic

▸ https://github.com/txus/kleisli

kleisli

require 'kleisli'

data = Maybe(csv_file).fmap do |value| begin Right(CSV.parse(value.read)) rescue ParseError => e Left(e.message) endend

Maybe

Either

validated = data.fmap do |value| errors = validate.call(value)

if errors Left(errors) else Right(value) endend

persisted = validated.fmap do |value| result = persist_data.call(value)

if result.success? Right(result.value) else Left(result.error) endend

persisted.fmap do |result| # do something with the resultend

persisted.or do |error| # do something with the errorend

FUNCTIONAL OBJECTS

stateless objects

objects that don’t change

respond to `#call` method

`#call` receives an input

and returns an output

object instantiation == hard :(

require 'dry-container'

app_container = Dry::Container.new

app_container.register(:database) { Database.new}

app_container.register(:validate_user) { ValidateUser.new}

app_container.register(:persist_user) { PersistUser.new}

require ‘dry-auto_inject'

Import = Dry::AutoInject.new { container(app_container)}

class PersistUser include Import[:database, :validate_user]

def call(input) input.fmap do |user| errors = validate_user.call(user)

if errors Left(errors) else Right(database[:users].insert(user)) end end endend

DOES IT MAKE ANY SENSE!?

Yes!(> ^_^ )>

We should keep exploring

and educating others

OO languages are here to stay

(for a while, at least)

THANKS FOR LISTENING!

▸ Piotr Solnica

▸ Software Consultant / Ruby expert

▸ solnic.eu

▸ @_solnic_

▸ github.com/solnic