3

Why Should You Curry?

 1 year ago
source link: https://code-pilot.me/why-should-you-curry
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Why Should You Curry?

By Noam Yadgar [Sun, 04 Dec 2022 08:14:08 GMT] [email protected]

Currying (named after Haskell Curry, sometimes known as partial-application)
is a powerful technique, borrowed straight from λ-Calculus.
It’s used mostly in the functional-programming paradigm but can easily be applied
to other paradigms and languages that support functions as first class citizens.

Pure functional languages like Haskell, are implicitly making use of currying
(every function’s parameter is currying the next function) while other languages
should implement the behavior explicitly.
In this article, I’ll be using Go, due to its simple syntax, strong type system, and explicitness.

My philosophy of studying, is to start from the outer shell and dig your way through,
so before you dive into the history of Currying, I think it’s best to start with code.

currying

Currying a general function to generate more specific functions:

Let’s start simply. Suppose we have a function that adds two integers:

func add(a, b int) int {
  return a + b
}

To make this a Curryable function we can do:

func add(a int) func(int) int {
  return func(b int) int {
    return a + b
  }
}

I admit that Go’s verboseness makes it look awkward. Other languages like Javascript
will make it look a bit more elegant:

const add = a => b => a + b 

In Haskell, it will be as simple as:

add a b = a + b 

For that matter, I think that Go’s syntax is very clear.
Since it’s expressing exactly what this function does.

  • It takes an int
  • Returns a function that takes another int
  • The returned function, returns the sum of the two integers
package main 

func add(a int) func(int) int {
  return func(b int) int {
    return a + b
  }
}

func main() {
  add5to := add(5)
  add3to := add(3)

  x1 := add5to(6) 
  x2 := add3to(1)

  fmt.Println(x1)
  fmt.Println(x2)
}

In the code above, I’ve generated two functions from of the add function.
I called them with some arguments and the output is:

> go run main.go 
11 
4 

Ok, I agree. This is complete overkill. I mean, having a language that
allows you to operate in this manner without explicitly returning functions is one thing.
But, writing a messy code of nested function returns, is just not clean.
Bad idea, I think we should end this article here…

Wait don’t go!

You’ve just discovered the power of Currying, you don’t just walk away :)
Sure, Go (as opposed to Haskell) doesn’t treat functions exactly as values
func() int is not equal to int, so we’ll have to explicitly return a func
to make our functions Curryable.

But instead of ruling out the idea of Currying in a language like Go,
let’s expand this pattern and see how it can make for a better code.

Flexibility and Reusability

Suppose we have this struct type:

type Employee struct {
  Id         string `json:"id"`
  Name       string `json:"name"`
  Department string `json:"department"`
  Salary     int    `json:"salary"`
}

We would like to load some data from outside the program,
and parse this data (Unmarshal) as our Employee type.
Let’s write a function that loads data from the file-system:

func LoadData(path, id string) ([]byte, error) {
  return os.ReadFile(path+id+".json")
}

And a function that will parse the data:

func ParseEmployee(data []byte) (Employee, error) {
  emp := Employee{}
  if err := json.Unmarshal(data, &emp); err != nil {
    return Employee{}, err 
  }
  return emp, nil
}

Combine everything:

func FetchEmployee(path, id string) (Employee, error) {
  data, err := LoadData(path, id) 
  if err != nil {
    return Employee{}, err 
  }
  return ParseEmployee(data)
}

Great, we have a tight little function that reads data from our file-system
and parses it as our Employee type.

What if we need our program to read data from various locations (for example:
Mongodb, remote storage like AWS S3, etc.).
Or even worse, what if our program needs to parse data of different formats? (like yaml)
And what if we don’t know this behavior in advance?

I think you can imagine how quickly our FetchEmployee function can become a nightmare
of case management and configurations.
We can build a much more flexible and reusable process by structuring our code in a Curryable manner.
Let’s start by defining this pattern in high-level types:

type Loader func(string) ([]byte, error)

type Parser func([]byte) (Employee, error)

type Fetcher func(string) (Employee, error)

Now, here is the cool part. If we can make a function that,
takes Parser and Loader types and return a Fetcher type,
We can easily generate different processes. It can look something like this:

FetchJsonEmployeeFromFS := makeFetcher(parseJsonEmployee, LoadFromFS)
FetchJsonEmployeeFromMongo := makeFetcher(parseJsonEmployee, LoadFromMongo)
FetchYamlEmployeeFromS3 := makeFetcher(parseYamlEmployee, LoadFromS3)

What is this voodoo? Let’s implement this function:

func makeFetcher(parse Parser, load Loader) Fetcher {
  return func(id string) (Employee, error) {
    data, err := load(id) 
    if err != nil {
      return Employee{}, err 
    }
    emp, err := parse(data)
    if err != nil {
      return Employee{}, err
    }
    return emp, nil
  }
}

I’d carefully say that it resembles the factory pattern found in object-oriented programming.
Only we’re not initiating instances, but processes.

Take a moment to appreciate the power of this function.
It can generate a Fetcher type function, and it doesn’t care about the internal
logic of the functions we’re passing to it.
A Parser type is any function that maps []byte to Employee, and a Loader
is any function that takes a string and returns a []byte of data.

So it doesn’t care if my data is being unmarshalled from a yaml file or
a json. And it doesn’t care about where and how the data is being loaded.
As long as the logic fits the three signatures (Parser, Loader, and Fetcher),
makeFetcher will combine the logic and return a Fetcher function.

Wait, your LoadData is not a Loader!

Ok, you’ve got me…

func LoadData(path, id string) ([]byte, error) {
  return os.ReadFile(path+id+".json")
}

This is not a Loader

type Loader func(string) ([]byte, error)

The Loader type seems a bit too “limited” for a function that should
load data from outside. What about configurations, connections, and all sorts of
stuff we might need to pass to fetch the data from outside?

Well, the same pattern can be used to derive a Loader type function.
Currying is not limited. It’s not even a specific pattern, but a technique.
As long as functions are first-class citizens, you can decorate them with all
the necessary things.

Think about it. In our simple example, when I’ve defined:

add5to := add(5)

add5to was defined as function of type func(int) int that holds the value 5
which it has inherited from the add function.
In the same way, we can pre-configure everything we need and Curry our
functions up to the simple Loader type function.

Let’s turn our LoadFile into a valid Loader type:

func makeFSLoader(path, ext string) Loader {
  return func(filename string) ([]byte, error) {
    return os.ReadFile(fmt.Sprintf("%s/%s.%s", path, filename, ext))
  }
}

Now we can easily and dynamically set file-system Loader functions:

loadJsonFromFS := makeFSLoader("./employees", "json")
loadYamlFromFS := makeFSLoader("./employees/ymls", "yaml")

In conclusion (some philosophy)

I’d like to draw your attention to the evolution we’ve experienced during this reading.
We’ve started with a simple concept, practiced mostly under the functional-programming domain,
applied it to a language of which it’s less commonly used, and pretty quickly saw
the huge potential for writing flexible, dynamic, reusable, and elegant code.

We very often like to categorize our systems and paradigms as completely diverged
branches that share only ancient ancestry. But in reality, those branches have
many meeting points and are far more involved with ideas being exchanged.
Paradigms are not signed, sealed delivered packages, but living creatures.

I think that Currying, a once concept exclusively reserved for pure functional
languages, finding its way to more and more imperative, C-style languages,
is a living evidence of the natural process of evolution occurring in programming languages.

I hoped you enjoyed this article. Please subscribe to receive updates on new posts.
See you next time, thank you!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK