📜 ⬆️ ⬇️

Entertaining functional programming in Ruby


This article focuses on a pointless journey through Ruby's degenerate form in an effort to learn more about functional programming, simplicity, and software interface design.

Suppose that the only way to represent a code is a lambda expression, and the only available data structure is an array:

square = ->(x) { x * x } square.(4) # => 16 person = ["Dave",:male] print_person = ->((name,gender)) { puts "#{name} is a #{gender}" } print_person.(person) 

These are the very basics of functional programming: functions are the only thing we have. Let's try to write something more similar to real code in the same style. Let's see how far we can go without much suffering.

Suppose we want to work with a database containing information about people, and someone has provided us with several functions for interacting with the internal repository. We want to add user interface and input validation.
')
Here is how we will contact the repository:

 insert_person.(name,birthdate,gender) # =>  id update_person.(new_name,new_birthdate,new_gender,id) delete_person.(id) fetch_person.(id) # =>  ,        

First, we need to be able to add a person to the database. In this case, the input data must pass the test. We will extract this data from the standard input stream (assuming that gets and puts are built-in functions and work as expected):

 puts "Name?" name = gets puts "Birthdate?" birthdate = gets puts "Gender?" gender = gets 


We need a function to validate the data and add it to the database. How can she look like? It should take the attributes of a person and return either id if the validation and insertion was successful, or an error message if something went wrong. Since we have no exceptions, no hash tables (only arrays), we will have to think creatively.

Let's agree that in our application, all business logic methods return an array of two elements: the first element is the value of the function when it is successfully completed, and the second element is a string with an error message. The presence or absence of a value ( nil ) in one of the cells of the array indicates the success or failure of the operation.

Now that we know what needs to be taken and what needs to be returned, let's start writing the function itself:

 add_person = ->(name,birthdate,gender) { return [nil,"Name is required"] if String(name) == '' return [nil,"Birthdate is required"] if String(birthdate) == '' return [nil,"Gender is required"] if String(gender) == '' return [nil,"Gender must be 'male' or 'female'"] if gender != 'male' && gender != 'female' id = insert_person.(name,birthdate,gender) [[name,birthdate,gender,id],nil] } 

If you do not know what String() , then this function returns an empty string if the value nil passed to it.

We want to use this function in a loop until the user provides valid data, like this:

 invalid = true while invalid puts "Name?" name = gets puts "Birthdate?" birthdate = gets puts "Gender?" gender = gets result = add_person.(name,birthdate,gender) if result[1] == nil puts "Successfully added person #{result[0][0]}" invalid = false else puts "Problem: #{result[1]}" end end 

Of course, we did not say that cycles cannot be used :) But suppose that we do not have them.

Loops are just functions (called recursively)


To loop, we simply wrap our code in a function and call it recursively until we get the desired result.

 get_new_person = -> { puts "Name?" name = gets puts "Birthdate?" birthdate = gets puts "Gender?" gender = gets result = add_person.(name,birthdate,gender) if result[1] == nil puts "Successfully added person #{result[0][0]}" result[0] else puts "Problem: #{result[1]}" get_new_person.() end } person = get_new_person.() 

We can assume that in our code there will be a lot of checks like if result[1] == nil , so let's wrap them in a function. The great thing about functions is that they allow you to reuse the structure, not the logic. The structure here is an error check and a call to one of two functions for success or failure.

 handle_result = ->(result,on_success,on_error) { if result[1] == nil on_success.(result[0]) else on_error.(result[1]) end } 

Now the get_new_person function uses a more abstract way to handle errors:

 get_new_person = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp result = add_person.(name,birthdate,gender) handle_result.(result, ->((id,name,birthdate,gender)) { puts "Successfully added person #{id}" [id,name,birthdate,gender,id] }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) } person = get_new_person.() 

Note that using handle_result allows handle_result to explicitly name variables instead of using array indexing. Now we can not only use the friendly name error_message , but also “split” the array into parts and use it as separate function parameters using the syntax of the form ((id,name,birthdate,gender)) .

So far so good. This code may look a little weird, but it is not wordy or confusing.

More functions - cleaner code


It may seem unusual that nowhere in our code there was a formal definition of the data structure for our “person”. We just have an array, and we agreed that the first element is the name, the second is the date of birth, etc. The idea is quite simple, but let's imagine that we need to add a new field: the title. What happens to our code if we try to do this?

The database now provides new versions of insert_person and update_person :

 insert_person.(name,birthdate,gender,title) update_person.(name,birthdate,gender,title,id) 

Change the add_person method:

 add_person = ->(name,birthdate,gender,title) { return [nil,"Name is required"] if String(name) == '' return [nil,"Birthdate is required"] if String(birthdate) == '' return [nil,"Gender is required"] if String(gender) == '' return [nil,"Gender must be 'male' or 'female'"] if gender != 'male' && gender != 'female' id = insert_person.(name,birthdate,gender,title) [[name,birthdate,gender,title,id],nil] } 

Since we are using a new field, we need to update and get_new_person . Khm:

 get_new_person = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp puts "Title?" title = gets.chomp result = add_person.(name,birthdate,gender,title) handle_result.(result, ->((name,birthdate,gender,title,id)) { puts "Successfully added person #{id}" [id,name,birthdate,gender,title,id] }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) } 

This shows the whole point of the strong connectivity of the application components. get_new_person does not have to worry about specific fields at all. The function should simply read them and then transfer them to add_person . Let's see how we can fix this if we move the code into several new functions:

 read_person_from_user = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp puts "Title?" title = gets.chomp [name,birthdate,gender,title] } person_id = ->(*_,id) { id } get_new_person = -> { handle_result.(add_person.(*read_person_from_user.()) ->(person) { puts "Successfully added person #{person_id.(person)}" person }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) } 

Now information about how we store data about a person is hidden in two functions: read_person_from_user and person_id . Now we don’t need to change get_new_person if we want to add more fields to the record.

If you are concerned about * in the code, here is a brief explanation: * allows you to give an array for the list of arguments and vice versa. In person_id we use the parameter list *_, id , which tells Ruby to put all the arguments, except the last, into the _ array (we are not interested in this array, therefore this name), and put the last one into the id variable. This only works in Ruby 1.9; in 1.8 only with the last argument you can use the syntax * . Then when we call add_person , we use * with the result read_person_from_user . Since read_person_from_user returns an array, we want to use this array as a list of arguments, since add_person accepts explicit arguments. That is what it does * . Fine!

If you go back to the code, you can note that read_person_from_user and person_id still quite strongly interconnected. They both know how we store data. Moreover, if we added new features for processing data from our database, we would have to use functions that also know about the internal structure of the array.

We need some kind of data structure.

Data structures are just functions.


In normal Ruby, we would have already organized a class or at least Hash by this point, but we cannot use them. Can we make a real data structure with only functions available? It turns out yes, we can, if we create a function that considers its first argument as an attribute of the data structure:

 new_person = ->(name,birthdate,gender,title,id=nil) { return ->(attribute) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title nil } } dave = new_person.("Dave","06-01-1974","male","Baron") puts dave.(:name) # => "Dave" puts dave.(:gender) # => "male" 

new_person acts as a constructor, but instead of returning an object (which we don’t have), it returns a function that, when invoked, can return us the values ​​of various attributes of a particular entry.

Compare with the implementation of the same behavior using the class:

 class Person attr_reader :id, :name, :birthdate, :gender, :title def initialize(name,birthdate,gender,title,id=nil) @id = id @name = name @birthdate = birthdate @gender = gender @title = title end end dave = Person.new("Dave","06-01-1974","male","Baron") puts dave.name puts dave.gender 

Interesting. The size of these pieces of code is about the same, but the version with the class uses special constructions . Special constructions are essentially the magic that a programming language provides. In order to understand this code, you need to know:


To understand the functional version, all you need to know is:


As I said, it seems interesting to me. We have two ways to do essentially the same thing, but one of them requires much more specialized knowledge from you than the other.

Well, now we have a real data structure. Let's change our code so that it works with it, not with arrays:

 read_person_from_user = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp puts "Title?" title = gets.chomp new_person.(name,birthdate,gender,title) } add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' id = insert_person.(person.(:name),person.(:birthdate),person.(:gender),person.(:title)) [new_person.(person.(:name),person.(:birthdate),person.(:gender),person.(:title),id),nil] } get_new_person = -> { handle_result.(add_person.(read_person_from_user.()), ->(person) { puts "Successfully added person #{person.(:id)}" person }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) } 

add_person now looks less beautiful because of the syntax for obtaining an attribute, but now we can easily add new fields, preserving the structure of the program.

Object orientation is only a function.


We can also make derived fields. Suppose we want to add a greeting to the user who indicated the title. We can make this an attribute:

 new_person = ->(name,birthdate,gender,title,id) { return ->(attribute) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title if attribute == :salutation if String(title) == '' return name else return title + " " + name end end nil } } 

Damn, yes, we can add the most real methods in OOP-style, if you want:

 new_person = ->(name,birthdate,gender,title,id) { return ->(attribute) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title if attribute == :salutation if String(title) == '' return name else return title + " " + name end elsif attribute == :update update_person.(name,birthdate,gender,title,id) elsif attribute == :destroy delete_person.(id) end nil } } some_person.(:update) some_person.(:destroy) 

While we are talking about OOP, let's add inheritance! Suppose we have an employee who is a person, but still has his own employee number:

 new_employee = ->(name,birthdate,gender,title,employee_id_number,id) { person = new_person.(name,birthdate,gender,title,id) return ->(attribute) { return employee_id_number if attribute == :employee_id_number return person.(attribute) } } 

We created classes, objects, and inheritance on functions alone in a few lines of code.

In a sense, an object in an object-oriented language is a set of functions that have access to a common set of data. It is easy to see why adding an object system to a functional language is considered trivial for those who understand functional languages. This is much easier than adding functions to an object-oriented language!

Although the syntax for accessing attributes is not very nice, the absence of classes does not cause me terrible suffering. Classes look more like syntactic sugar than some serious concept.

However, problems may arise with changing data. See how verbose the add_person function add_person . She calls insert_person to put a record in the database and gets the ID back. Then we need to create a completely new record in order to just set the ID. In a classic OOP, we would simply write person.id = id .

Is the ability to change the state advantage of this design? I would say that the compactness of this design is its main advantage, and the fact that it is the variability of the object that makes the structure compact is just an accident. Only if we use a system with an extreme lack of memory and terrible garbage collection, can we be concerned about the creation of new objects. We really will be annoyed by the useless creation of new and new objects from scratch. But since we already know how to add functions to, uh, our function, let's try to implement this compact syntax:

 new_person = ->(name,birthdate,gender,title,id=nil) { return ->(attribute,*args) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title if attribute == :salutation if String(title) == '' return name else return title + " " + name end end if attribute == :with_id # <=== return new_person.(name,birthdate,gender,title,args[0]) end nil } } 

Now add_person even simpler:

 add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' id = insert_person.(person.(:name),person.(:birthdate),person.(:gender),person.(:title)) [new_person.(:with_id,id),nil] # <==== } 


It looks, of course, not as clean as person.id = id , but it looks decent enough to be readable. The code from it became only better.

Namespaces are only functions.


What I really miss is namespaces. If you have ever programmed in C, you probably know how the code becomes littered with functions with complex prefixes in order to avoid name clashes. Of course, we could do something similar here, but it would be much more pleasant to have the correct namespaces, such as those provided by modules in Ruby or object literals in JavaScript. I would like to do this without adding new language features. The simplest way is to implement something like a display. We can already access the explicit attributes of the data structure, so now it’s enough to come up with a more general way to do this.

At the moment, the only data structure we have is an array. We do not have array methods, because we do not have classes.

Ruby arrays are actually tuples, and the most common operation we can do on them is data retrieval. For example:

 first = ->((f,*rest)) { f } # or should I name this car? :) rest = ->((f,*rest)) { rest } 

We can model a display as a list, treating it as a list with three elements: a key, a value, and the remainder of the display. Let's avoid the "OOP-style" and leave only the pure "functional":

 empty_map = [] add = ->(map,key,value) { [key,value,map] } get = ->(map,key) { return nil if map == nil return map[1] if map[0] == key return get.(map[2],key) } 

Example of use:

 map = add.(empty_map,:foo,:bar) map = add.(map,:baz,:quux) get.(map,:foo) # => :bar get.(map,:baz) # => :quux get.(map,:blah) # => nil 

This is enough to implement namespaces:

 people = add.(empty_map ,:insert ,insert_person) people = add.(people ,:update ,update_person) people = add.(people ,:delete ,delete_person) people = add.(people ,:fetch ,fetch_person) people = add.(people ,:new ,new_person) add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' id = get(people,:insert).(person.(:name), person.(:birthdate), person.(:gender), person.(:title)) [get(people,:new).(:with_id,id),nil] } 

new_person course, we could replace the implementation of new_person map, but it’s more convenient to have an explicit list of attributes that we support, so leave new_person as is.

Last focus. include is a great Ruby feature that allows modules to be inserted into the current scope so as not to use explicit namespace resolution. Can we do it here? Close:

 include_namespace = ->(namespace,code) { code.(->(key) { get(namespace,key) }) } add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' include_namespace(people, ->(_) { id = _(:insert).(person.(:name), person.(:birthdate), person.(:gender), person.(:title)) [_(:new).(:with_id,id),nil] } } 

Well, this may be too much, but it is still interesting to use include only to print less, when we can achieve the same behavior simply by using functions.

What have we learned?


Using just a few basic language constructs, we were able to make a new programming language. We can create real data types, namespaces, and we can even use OOP without explicit support for it with language constructs. And we can do it in about the same amount of code as if we only used the built-in Ruby tools. The syntax is a bit more verbose than in normal Ruby, but still not so bad. We could even write real code using this “truncated” version of Ruby. And it would not look absolutely terrible.

Will it help in everyday work? I think this is a lesson in simplicity. Ruby is overloaded with highly specialized constructs, complex syntax and metaprogramming, but we managed to accomplish a lot without even using classes! Maybe your problem can be solved in a simpler way? Maybe you should just rely on the most obvious parts of the language, rather than trying to use all the "coolest features"?

Source: https://habr.com/ru/post/148076/


All Articles