Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Fun with Functions - Frank Müller - Loodse

GoDays
January 23, 2020

Fun with Functions - Frank Müller - Loodse

When getting in touch with Go for the first time it shows a very imperative nature. Oh, and yes, types can have methods and the concurrency is a powerful feature too. But there's another paradigm, a very old one – functional programming. Sure, Go is no functional programming language. But the functions in Go are very powerful, just to do their work, as type, as arguments or return values, for anonymous closures, and to send them over channel. Enjoy my travel through the world of Go functions.

GoDays

January 23, 2020
Tweet

More Decks by GoDays

Other Decks in Technology

Transcript

  1. Powerful - Flexible - Elegant
    Fun with Functions

    View full-size slide

  2. Solution Engineer @ Loodse
    Frank Müller
    @themue @themue

    View full-size slide

  3. With and Without Names
    Simple Functions

    View full-size slide

  4. Simple functions are well known
    func Status() int {
    return status
    }
    func SetStatus(s int) {
    status = s
    }

    View full-size slide

  5. Arguments and return values are flexible
    func HasManyArgs(a int, b, c string, foos ...Foo) {
    ...
    }
    func ReturnsMultipleValues() (int, error) {
    ...
    return 12345, nil
    }

    View full-size slide

  6. Calling them is simple
    HasManyArgs(12345,”foo”, “bar”, aFoo, anotherFoo)
    var i int
    var err error
    i, err = ReturnsMultipleValues()

    View full-size slide

  7. Functions don’t always need a name
    myNewSlice := All(myOldSlice, func(in string) string {
    return strings.ToUpper(in)
    })
    hits := Find(myNewSlice, func(in string) bool {
    l := len(in)
    return l >= 3 && l <= 10
    })

    View full-size slide

  8. Their creation context will be captured
    func mkMultiAdder(a, b int) func(int) int {
    m := a * b
    // Closure.
    return func(i int) int {
    return m + i
    }
    }
    centuryAdder := mkMultiAdder(2, 1000)
    thisYear := centuryAdder(20) // 2020

    View full-size slide

  9. func TestFoo(t *testing.T) {
    check := func(ok bool, msg string) {
    if !ok {
    t.Fatal(msg)
    }
    }
    ...
    check(a == b, “a should be equal to b”)
    check(a%2 == 0, “a should be even”)
    }
    They can help inside of functions

    View full-size slide

  10. f, err := os.Open(“file.txt”)
    if err != nil {
    ...
    }
    defer f.Close()
    ...
    Function calls can be deferred and called on leaving

    View full-size slide

  11. f, err := os.Open(“file.txt”)
    if err != nil {
    ...
    }
    defer log.Printf(“closed file: %v”, f.Close()) // !!!
    ...
    But take care, only outer call is deferred

    View full-size slide

  12. // Fire and forget.
    go doSomethingOnce(data)
    // Processing in background.
    go doSomethingForAll(inC, outC)
    // Run until cancelled.
    go doForever(ctx)
    Goroutines are only functions too

    View full-size slide

  13. Fixed and Optional
    Typical Functions

    View full-size slide

  14. func All(
    ins []string,
    process func(string) string) []string {
    outs := make([]string, len(ins))
    for i, in := range ins {
    outs[i] = process(in)
    }
    return outs
    }
    Function types may be implicit

    View full-size slide

  15. type Filter func(string) bool
    func Find(ins []string, matches Filter) []string {
    var outs []string
    for _, in := range ins {
    if matches(in) {
    outs = append(outs, in)
    }
    }
    return outs
    }
    But also may have a named type

    View full-size slide

  16. These named types can be options too
    ● Think of a complex type with a number of fields
    ● Concurrent types providing internal services
    ● Database clients
    ● Network servers
    ● ...
    ● These fields shall have default values
    ● These fields also shall be optionally configurable
    ● Functions may help in an elegant way

    View full-size slide

  17. type Client struct {
    address string
    timeout time.Duration
    poolsize int
    logging bool
    Initializer func(conn *Connection) error
    ...
    }
    type Option func(c *Client) error
    How does this Option() help?

    View full-size slide

  18. func NewClient(options ...Option) (*Client, error) {
    c := &Client{ ... }
    for _, option := range options {
    if err := option(c); err != nil {
    return nil, err
    }
    }
    ...
    return c
    }
    Construct the Client with options

    View full-size slide

  19. func WithTimeout(timeout time.Duration) Option {
    return func(c *Client) error {
    if timeout < minTimeout {
    return ErrTimeoutTooLow
    }
    c.timeout = timeout
    return nil
    }
    }
    Options with arguments and validation

    View full-size slide

  20. client, err := database.NewClient(
    database.WithAddress(“db.mycompany.com:6379”),
    database.WithPoolsize(16),
    database.WithoutLogging(),
    database.WithInitializer(
    func(conn *database.Connection) error {
    ...
    }),
    )
    All together from user perspective

    View full-size slide

  21. With and Without Interfaces
    Methodological Functions

    View full-size slide

  22. type Counter struct {
    value int
    }
    func (c *Counter) Incr() {
    c.value++
    }
    func (c Counter) Get() int {
    return c.value
    }
    Methods are simply functions with a receiver

    View full-size slide

  23. type Adder struct {
    base int
    }
    func NewAdder(base int) Adder {
    return Adder{base}
    }
    func (a Adder) Add(i int) int {
    return a.base + i
    }
    Really? Yes, be patient

    View full-size slide

  24. adder := NewAdder(2000)
    add := adder.Add
    thisYear := add(20) // 2020
    And where is the function?

    View full-size slide

  25. type IntGetter interface {
    Get() int
    }
    func (c Counter) Get() int {
    return c.value
    }
    func (a Age) Get() int {
    return int(time.Now().Sub(a.birthday) / 24)
    }
    Method sets aka interfaces are flexible

    View full-size slide

  26. type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
    }
    type HandlerFunc func(ResponseWriter, *Request)
    func (f HandlerFunc) ServeHTTP(
    w ResponseWriter, r *Request) {
    f(w, r)
    }
    Can function types implement interfaces too? Sure!

    View full-size slide

  27. Let’s replace the Logic
    Handle Events

    View full-size slide

  28. type Event struct {
    ...
    }
    type Handler func(evt Event) (Handler, error)
    type Machine struct {
    handle Handler
    }
    There events, handlers, and machines

    View full-size slide

  29. func (m *Machine) Next(evt Event) error {
    handler, err := m.handle(evt)
    if err != nil {
    return err
    }
    m.handle = handler
    }
    Let the events flow

    View full-size slide

  30. func home(evt Event) (Handler, error) {
    switch evt.Kind {
    case takeTrain:
    return work, nil
    case sleep:
    return bed, nil
    default:
    return nil, errors.New(“illegal event”)
    }
    }
    Example: Game of Life (1/3)

    View full-size slide

  31. func work(evt Event) (Handler, error) {
    switch evt.Kind {
    case takeTrain:
    return home, nil
    default:
    return nil, errors.New(“illegal event”)
    }
    }
    Example: Game of Life (2/3)

    View full-size slide

  32. func bed(evt Event) (Handler, error) {
    switch evt.Kind {
    case takeTrain:
    return wake, nil
    default:
    return nil, errors.New(“illegal event”)
    }
    }
    Example: Game of Life (3/3)

    View full-size slide

  33. ● For real scenarios work with structs and methods
    ● Struct contains the additional data the states can act on
    ● Individual methods with the same signature represent the handlers
    ● func (m *Machine) home(evt Event) (Handler, error)
    ● func (m *Machine) work(evt Event) (Handler, error)
    ● func (m *Machine) bed(evt Event) (Handler, error)
    ● func (m *Machine) sports(evt Event) (Handler, error)
    ● ...
    Trivial example

    View full-size slide

  34. Sequential Processing
    Traveling Functions

    View full-size slide

  35. func (mt *myType) backend() {
    for {
    select {
    case one := <-mt.oneC:
    ...
    case two := <-mt.twoC:
    ...
    }
    }
    }
    You know this pattern

    View full-size slide

  36. Handle concurrency with care
    ● Concurrency is a powerful mechanism
    ● Unsynchronized access to data quickly leads to troubles
    ● Go provides goroutines for work and channels for communication
    ● Static typing needs individual channels for individual types
    ● So overhead of goroutine loop and helper types to transport reply channels
    grows
    ● Functions as types may help here too

    View full-size slide

  37. type ProcessFunc func(string) string
    type Processor struct {
    ctx context.Context
    process ProcessFunc
    buffer []string
    actions chan func()
    }
    Think of a synchronized buffered text processor

    View full-size slide

  38. func New(ctx context.Context, pf ProcessFunc) *Processor {
    p := &Processor{
    ctx: ctx,
    process: pf,
    actions: make(chan func(), queueSize),
    }
    go p.backend()
    return p
    }
    Constructing is simple

    View full-size slide

  39. func (p *Processor) backend() {
    for {
    select {
    case <-p.ctx.Done():
    return
    case action := <-p.actions:
    action()
    }
    }
    }
    Actions are performed in backend method

    View full-size slide

  40. func (p *Processor) Push(lines ...string) {
    p.actions <- func() {
    for _, line := range lines {
    p.buffer = append(p.buffer, p.process(line))
    }
    }
    }
    Logic is defined in public methods

    View full-size slide

  41. func (p *Processor) Pull() (string, error) {
    var line string
    var err error
    done := make(chan struct{})
    p.actions <- func() {
    defer close(done)
    if len(p.buffer) == 0 {
    err = errors.New(“empty buffer”)
    return
    }
    ...
    Return values are possible too (1/2)

    View full-size slide

  42. ...
    line = p.buffer[0]
    p.buffer = p.buffer[1:]
    }
    <-done
    return line, err
    }
    Return values are possible too (2/2)

    View full-size slide

  43. ● Extra unbuffered action channel for synchronous methods
    ● Private methods for runtime logic
    ● Taking care if the backend goroutine still runs
    ● Method doSync(action func()) error for synchronous actions
    ● Method doAsync(action func()) error for asynchronous actions
    ● Stop() error method to close the context by wrapping it with
    context.WithCancel()
    ● Err() error method to return internal status
    Extensions help

    View full-size slide

  44. Powerful - Flexible - Elegant
    Summary

    View full-size slide

  45. ● Very simple to understand
    ● Powerful variants
    ● Flexibly usable by composition
    ● Closures are an elegant way to encapsulate logic with access to their
    context
    ● Methods are functions knowing the instance they are defined for
    ● Functions via channels help to keep concurrency maintainable
    Functions are primary ingredients of the language

    View full-size slide