samber / mo

🦄 Monads and popular FP abstractions, powered by Go 1.18+ Generics (Option, Result, Either...)
https://pkg.go.dev/github.com/samber/mo
MIT License
2.61k stars 85 forks source link

Do notation #43

Closed taman9333 closed 3 months ago

taman9333 commented 3 months ago

Description

This PR introduces do-notation to simplify error handling and improve the readability of monadic operations.

Example:

package main

import (
    "errors"
    "fmt"
    "github.com/samber/mo"
)

func validateBooking(params map[string]string) mo.Result[map[string]string] {
    if params["guest"] != "" && params["roomType"] != "" {
        return mo.Ok(params)
    }
    return mo.Err[map[string]string](errors.New("validation failed"))
}

func createBooking(guest string) mo.Result[string] {
    if guest != "" {
        return mo.Ok("Booking Created for: " + guest)
    }
    return mo.Err[string](errors.New("booking creation failed"))
}

func assignRoom(booking string, roomType string) mo.Result[string] {
    if roomType != "" {
        return mo.Ok("Room Assigned: " + roomType + " for " + booking)
    }
    return mo.Err[string](errors.New("room assignment failed"))
}

// This could be a service package that performs the entire process
func bookRoom(params map[string]string) mo.Result[[]string] {
    return mo.Do(func() []string {
        // Validate booking parameters
        values := validateBooking(params).MustGet()

        // Create booking
        booking := createBooking(values["guest"]).MustGet()

        // Assign room
        room := assignRoom(booking, values["roomType"]).MustGet()

        // Return success with booking and room details
        return []string{booking, room}
    })
}

func main() {
    params := map[string]string{
        "guest":   "Foo",
        "roomType": "Suite",
    }

    result := bookRoom(params)
    if result.IsError() {
        fmt.Println("Error:", result.Error())
    } else {
        fmt.Println("Success:", result.MustGet())
    }
}

Benefits:

Without the do-notation the code becomes much more verbose and less maintainable due to the explicit error handling after each step. By using do-notation, we centralize the error handling logic, resulting in cleaner and more concise code.

Comparison of createService Function:

without do-notation

func bookRoom(params map[string]string) mo.Result[[]string] {
    values, err := validateBooking(params).Get()
    if err != nil {
        return mo.Err[[]string](err)
    }

    booking, err := createBooking(values["guest"]).Get()
    if err != nil {
        return mo.Err[[]string](err)
    }

    room, err := assignRoom(booking, values["roomType"]).Get()
    if err != nil {
        return mo.Err[[]string](err)
    }

    return mo.Ok([]string{booking, room})
}

With do-notation

func bookRoom(params map[string]string) mo.Result[[]string] {
    return Do(func() []string {
        values := validateBooking(params).MustGet()
        booking := createBooking(values["guest"]).MustGet()
        room := assignRoom(booking, values["roomType"]).MustGet()
        return []string{booking, room}
    })
}
taman9333 commented 3 months ago

@samber

samber commented 3 months ago

hi @taman9333

I like the idea.

Can you explain why you named it "Do" instead of "Try" or other? Do does not seem meaningful to me. But I might be wrong.

taman9333 commented 3 months ago

@samber I am just following the same naming convention introduced in Haskell Do you prefer a different name?

taman9333 commented 3 months ago

~~@samber I've fixed those Testable Examples. Let me know if you have any other comments. You can merge the PR if you don't have any concerns~~ I have reverted fixing Testable Examples as I have found you fixed that

samber commented 3 months ago

Yes, I made a fix, while merging another contrib recently.

Thanks for your work