Conclusion - Becoming Functional (2014)

Becoming Functional (2014)

Chapter 10. Conclusion

I hope that you’ve found this book to be a helpful stepping-stone toward functional programming. Most important, I hope that it has demystified some of the concepts and shown how you might implement them without switching to a purely functional language.

I also hope that you can take some of the early concepts that might actually provide you the greatest benefit and apply those to your everyday job, helping you write less code while implementing more functionality.

From Imperative to Functional

At this point, you should have the knowledge and the understanding of how to transform your current imperative code into functional code. As you start your transition, you will want to break it down into steps. Let’s look at the transitional steps and recap how to implement those concepts:

1. Introduce higher-order functions.

2. Convert existing methods into pure functions.

3. Convert loops over to recursive/tail-recursive methods (if possible).

4. Convert mutable variables into immutable variables.

5. Use pattern matching (if possible).

Introduce Higher-Order Functions

The first step to take is to introduce some higher-order functions, as you saw in Chapter 2. To accomplish this in Java, you either use pre-existing libraries such as Guava or create your own interface like the Function1 interface so that you can encapsulate functionality to be passed to another function.

As demonstrated in Chapter 2, you can also begin to convert to a language such as Groovy, which enables you to use higher-order functions while still keeping the basic Java syntax. The advantage is that you don’t need to rewrite your entire code base in order to convert.

If you cannot integrate with a non-type-safe language such as Groovy, I would suggest starting to integrate with another language, such as Scala, to keep your type safety but still begin integrating functional concepts.

This has the largest benefit, because you’ll be able to immediately start taking advantage of code reuse. You’ll start seeing loops where you can abstract the looping logic and just start passing a higher-order function into the looping logic.

Convert Existing Methods into Pure Functions

The next step is to start converting your normal methods into pure functions, as I showed in Chapter 3. These should actually be fairly straightforward to convert—and as you begin converting them, you’ll find how much easier it is to write tests for those functions.

This is, again, one step that will have a lot of benefit right up front. You’ll begin to reduce your functions, which will make them even more testable and more understandable. This is really one of the first times that the concept of expressiveness will come up. As your decompose your functions into smaller, more pure functions, they will inherently become more expressive. Expressiveness reflects how much meaning each line of code has; the more that can be understood per one line of code, the more expressiveness it has.

Convert Loops to Tail/Recursive-Tail Methods

You don’t want to jump directly into immutable variables, so the first thing to do is convert your looping structures over to recursive—specifically, tail-recursive if you can—methods. The caveat here is that some languages, such as Java, don’t support tail recursion. If you can use it without worrying about buffer overflow, try it.

Recursion is something that many people find unfamiliar at first, but after you get into a functional mindset and practice with it, you’ll get much better at seeing the conceptual framework. You’ll see iteration via recursion instead of just normal iterative loops. The nice thing about recursion is that you’ll be able to truly test your loops to make sure that you’re looping over what you want. You’ll also be able to more fully test them.

Convert Mutable Variables into Immutable Variables

One of the last things you can do—without switching to a fully functional language—is move your normal mutable variables into immutable variables. This has many different implications, including the ability to do highly concurrent applications without locking variables.

Immutable variables are one of the more difficult things to get used to working with because we, as developers, are so accustomed to changing variables over time. The positive for not changing the variable is that you have no concern that a variable has “changed out from under you.”

What Next?

After you’ve done these things, generally you will have exhausted most of your functional abilities in the language you’re using. The next step, then, is to move to a more functional language, such as Scala or Groovy, or to a fully functional language, such as Erlang or Clojure. Whatever you choose, the concepts and ways to program in the functional paradigm will remain the same.

New Design Patterns

There are a few different design patterns based on some of the concepts we’ve seen throughout this book:

§ Message passing for concurrency (actor model for concurrency)

§ The Option pattern (extension of Null Object pattern)

§ Object to Singleton method purity

Let’s look at some of these design patterns and how we can actually use them in our day-to-day jobs.

Message Passing for Concurrency

When we think about concurrency, we think about a thread that starts, processes an amount of work, and then exits. Sometimes we think of a thread pool to which we submit jobs to be executed. However, with message passing we can actually send a message, which is then interpreted by the running thread to do processing.

The big difference between message passing and thread pools is that with the latter, an individual job must be created and executed, whereas in message passing you can have a thread that exists for a long period of time, and send messages to that thread to tell it what operations to perform. This allows the threads to communicate without blocking.

The Option Pattern (Extension of Null Object Pattern)

The Null Object pattern in Java is a way you can provide an object to be executed, but the object does nothing when executed and thus removes the need for null. An extension of that pattern is the Option pattern. The concept, as we saw in earlier chapters, is that you have an Optioninterface that is implemented by a Some and a None case; then with pattern matching, you are able to extract when the Option has Some or do an else if there is None.

Even if we don’t have pattern matching, we can still use the Option pattern by creating a getOrElse(T obj) method on the Option interface. This way, you can actually pass something to use in the event that you have None. All of the classes are listed in Example 10-1.

Example 10-1. The implementation of the Option pattern

publicinterfaceOption<T> {

public T getOrElse(T defObj);

}

publicclassSome<T> implements Option<T> {

privatefinal T obj;

public Some(T obj) {

this.obj = obj;

}

public T getOrElse(T defObj) {

return (this.obj == null) ? defObj : this.obj;

}

}

publicclassNone<T> implements Option<T> {

public T getOrElse(T defObj) {

return obj;

}

}

Now we can use this by passing a new Some<String>("Foo") or a new None<String>(), which then forces the method accepting the Option<String> to do a getOrElse. This means that it will always check for nullity so long as the underlying object is not null.

Object to Singleton Method Purity

Generally in object-oriented programming (OOP), our functions will contain lots of functional logic. In many instances, this functionality may bleed throughout an entire class. This means that you will eventually need the class to be instantiated to test even the most basic functionality.

Instead, when we want to have a set of functionality, we should create our static methods and then have our instance methods call those directly. This allows us to maintain function purity while being able to have OOP expressiveness. Example 10-2 shows a method on the instance thatonly calls a static method.

Example 10-2. The singleton method purity

publicclassFoo {

publicstatic String bar(Foo f) { return f.toString(); }

public String bar() { return Foo.bar(f); }

}

Putting It All Together

Throughout this book, I’ve tried to convey the principles of functional programming by showing you how to start with an imperative paradigm and move to the functional one. Instead of refactoring, we’re going to put all of these principles into action by building an example from the ground up.

Over the next few pages, we’re going to work through an example of a very simplistic database. Here is a very basic overview of the functionality that we’ll be implementing; it will be a menu-driven system, so it’s not meant to be a full-fledged database. It will have the following capabilities:

§ Can create a table

§ Can insert a record

§ Can delete a record (by ID)

§ Can list all records

§ Can query by one field

We’re going to use Scala, and we’ll get started by working with the initial application object shown in Example 10-3.

Example 10-3. The fDB.scala file

importscala.annotation.tailrec

objectfDB {

val options =Map[String, CommandLineOption](

"create" -> newCommandLineOption("Create Table", Database.createTable),

"describe" -> newCommandLineOption("Describe Table", Database.describeTable),

"insert" -> newCommandLineOption("Insert Record", Database.insert),

"delete" -> newCommandLineOption("Delete Record", Database.delete),

"select" -> newCommandLineOption("Select Record", Database.select),

"exit" -> newCommandLineOption("Exit", db => sys.exit)

)

@tailrec

def mainLoop(database :Database) :Unit = mainLoop(

CommandLine.optionPrompt(options) match {

caseSome(opt) => opt.exec(database)

case_=> { println("Invalid option"); database }

}

)

def main(args :Array[String]) = {

mainLoop(newDatabase(Map()))

}

}

The variable options is a mapping of all the possible options that a user can enter. These options are create, describe, insert, delete, select, and exit. For each one, we’ll create a CommandLineOption object, shown in Example 10-4, that contains a description and an executable function.

We’ll also create a mainLoop that will execute the CommandLine (Example 10-5), which will prompt a user based on the options available and try to grab that CommandLineOption.

If the option doesn’t exist, we make no changes and let the user know that she selected an invalid option. Otherwise, we will execute the CommandLineOption executable function, which will perform the necessary changes to the Database object, and which we will continue using in our tail-recursive function.

Finally, we have our main function, which will call into our mainLoop with a new blank Database object.

In Example 10-4, you can see the basic CommandLineOption object. It contains a name to be used for display purposes so that the user knows what the option does, and the exec, which will take a Database object, perform some operation based on that object, and then return either thatDatabase or a new one.

Example 10-4. The CommandLineOption.scala file

classCommandLineOption(val name :String, val exec :Database => Database)

The CommandLine object in Example 10-5 performs all of our prompting and printing functionality. We have a wrapOutput function, which will take some wrapping and some output that we want to display. We will then print the wrapping, followed by the output and the wrappingagain, so that we have a nice separation of data.

The next function is our optionPrompt function, which takes a mapping of input to CommandLineOption; this allows us to print out the mapping and then ask the user for the input. We will print in the format option) CommandLineOption.name and then ask the user to give us an input.

Our last function is a generic prompt function, which will print a message to the user and wait for her to input a line of data.

Example 10-5. The CommandLine.scala file

objectCommandLine {

def wrapOutput(wrapper :String, output :String) :Unit = {

println(wrapper)

print(output)

println(wrapper)

}

def optionPrompt(options :Map[String, CommandLineOption]) :

Option[CommandLineOption] = {

println()

println("----[Options]----")

options.foreach(option => println(option._1 + ") " + option._2.name))

options.get(prompt("Action").toLowerCase)

}

def prompt(msg :String) :String = {

print(msg + ": ")

readLine()

}

}

Our Database class in Example 10-6 is really straightforward—it has a set of tables, which will be a map of table name to Table. We’ll discuss the Table object in Example 10-7; for now, get to the Database object (singleton).

Example 10-6. The Database.scala file

objectDatabase {

def createTable(database :Database) :Database = {

newDatabase(database.tables +

(CommandLine.prompt("Table Name") -> Table.create()))

}

def describeTable(database :Database) :Database = {

database.tables.get(CommandLine.prompt("Table Name")) match {

caseSome(table) => table.describe()

case_=> println("Table does not exist")

}

database

}

def insert(database :Database) :Database = {

val tableName =CommandLine.prompt("Table Name")

database.tables.get(tableName) match {

caseSome(table) => {

newDatabase(database.tables + (tableName -> table.insert()))

}

case_=> { println("Table does not exist"); database }

}

}

def select(database :Database) :Database = {

database.tables.get(CommandLine.prompt("Table Name")) match {

caseSome(table) => table.select()

case_=> println("Table does not exist")

}

database

}

def delete(database :Database) :Database = {

val tableName =CommandLine.prompt("Table Name")

database.tables.get(tableName) match {

caseSome(table) =>newDatabase(

database.tables + (tableName -> table.delete()))

case_=> { println("Table does not exist"); database }

}

}

}

caseclassDatabase(tables :Map[String, Table]) {}

Our first method, create, will do just that—create a new table. We implement this by prompting for a table name that we will then use in our Table Name-to-Table mapping. We then use the function create to create a new table. You’ll notice that we’re adding this new association to our existing table mapping and creating a new Database object with this new table mapping.

Next, we have a describeTable method, which allows us to print out all of the fields from a specific table. Notice that we use the pattern match with the Option pattern to get the table by name and print an error if the table doesn’t exist.

In the insert method, we get the table and create a new database with the table replaced in the map. The important thing here is that the table we’re replacing it with will be the Table object’s insert return in Example 10-7.

The select method gets the table from which the user wants to select; it then uses a pattern match on the Option pattern again to perform the select on that table or print an error if the table doesn’t exist.

Finally, the delete method gets the table from which the user wants to delete, uses the pattern match on the Option pattern, and then passes to the table for the delete.

Our Table class in Example 10-7 has quite a few pieces to it. Let’s look at the class first, and notice the use of the static method calls from the instance methods.

Example 10-7. The Table.scala file

objectTable {

def createFields(count :Int,

fields :List[String]) :List[String] =if(count <= 0) {

fields

} else {

createFields(count - 1, fields ::: List(CommandLine.prompt("Field")))

}

def create() :Table = newTable(

createFields(

CommandLine.prompt("Number of fields").toInt,

List()

),

Map(),

1

)

def insert(table :Table) :Table = newTable(

table.fields,

table.records + (table.id -> Record.create(table.fields, Map())),

table.id + 1

)

def describe(table :Table) :Table = {

println("(implied) id")

table.fields.foreach(field => println(field))

table

}

def select(table :Table) :Table = {

CommandLine.prompt("Filter By Field? (y/n)").toLowerCase match {

case "y" => selectWithFilter(table)

case "n" => selectAll(table)

case_=> { println("Invalid selection"); select(table); }

}

}

def selectAll(table :Table) :Table = {

table.records.foreach(record => record._2.print(table.fields, record._1))

table

}

def selectWithFilter(table :Table) :Table = {

performFilter(

table,

CommandLine.prompt("Filter Field"),

CommandLine.prompt("Field Value")

).foreach(record =>

record._2.print(table.fields, record._1)

)

table

}

def performFilter(table :Table,

fieldName :String,

fieldValue :String) :Map[Long, Record] = {

if(fieldName == "id") {

table.records.get(fieldValue.toLong) match {

caseSome(record) =>Map(fieldValue.toLong -> record)

case_=>Map()

}

} else {

table.records.filter(record =>

record._2.fieldValues.get(fieldName) match {

caseSome(value) => value == fieldValue

case_=>false

}

)

}

}

def delete(table :Table) :Table = {

newTable(table.fields, table.records - CommandLine.prompt("ID").toLong, table.id)

}

}

caseclassTable(fields :List[String], records :Map[Long, Record], id :Long) {

def delete() :Table = {

Table.delete(this)

}

def select() :Table = {

Table.select(this)

}

def insert() :Table = {

Table.insert(this)

}

def describe() :Table = {

Table.describe(this)

}

}

We have a createFields method that will create all of our fields in a tail-recursive manner. Notice that we don’t actually need to use a pattern match to do tail recursion. We also have the create method, which asks for the number of fields and then calls into createFields to create the list of fields.

We also have an insert method, which will call into Record (shown in Example 10-8) to ask for each individual field value. We then add the record to our map with the id from the Table and create a new table with the new map and increment the id for the new table.

Our describe method iterates over each field and prints it out so that we know the table structure.

Next, we have our select method, which asks the user if she wants to filter the records for which she’s looking. Depending on which option she selects, we will go into either selectAll or selectWithFilter.

The selectAll iterates over each record and calls print. The selectWithFilter asks the user which field she wants to filter on and the value for which she’s looking. We then call into the performFilter method, which will return a map of only matching records and print out those records.

Our performFilter method splits on the field. If the user asks for the id field, we can directly access it based on the map’s key; otherwise, we will perform a filter on the records map to find the records that match. Notice that we can pattern-match in the case that the field is missing, and instead of blowing up, we’re just not going to match.

Finally, we have the delete method, which asks the user what id she wants to remove, and we remove it from the records map.

Our last Record class in Example 10-8 is fairly simple, but again let’s look at the class first.

Example 10-8. The Record.scala file

objectRecord {

def create(fields :List[String],

fieldValues :Map[String, String]) :Record = fields match {

caseList() =>newRecord(fieldValues)

case f :: fs =>

create(

fs,

fieldValues + (f -> CommandLine.prompt("Value [" + f + "]"))

)

}

def print(fields :List[String], id :Long, record :Record) :Unit = {

def_print(fieldList :List[String], output :String) :String = fieldList match {

caseList() => output

case f :: fs =>_print(fs, output + f + ": " + record.fieldValues(f) + "\n")

}

CommandLine.wrapOutput("------------", "id: " + id + "\n" + _print(fields, ""))

}

}

caseclassRecord(fieldValues :Map[String, String]) {

def print(fields :List[String], id :Long) :Unit = {

Record.print(fields, id, this)

}

}

We have a print method on the class itself that calls into the static print method, passing the list and id of the object along with itself.

In the singleton, we have two primary methods. The first is the create method, which uses tail recursion to iterate over the field list, asking for each field input. After all fields have been asked for, we create a new Record with the map we’ve been building through our recursive function.

The second method, print, takes the list of fields, the id of the record, and the record itself. We then create a nested function, _print, which does a tail-recursive iteration to create an output string that will contain each field and value. The print method uses the inner _print method and passes the output to our CommandLine object’s wrapOutput method, which then nicely prints out the object.

We now have a small functional database that we can use for simple queries. Our database utilizes all of the concepts of functional programming—from higher-order functions to immutability.

Conclusion

Throughout this chapter, we covered how you can start making the transition from a legacy imperative style to a functional style of programming. We also looked at both new design patterns and extensions of existing design patterns.

Finally, we wrote a simple, functional database in Scala. In doing so, we used first-class functions and functional OOP through the use of our CommandLineOption object. We also used pattern matching to determine whether the input option was valid. In addition, we used pure functions, recursion, and immutability throughout the application. Even when creating/updating/deleting records, we were able to apply immutability using recursion.

TAKING IT A STEP FURTHER

Here are some ideas to consider if you want to try to expand upon this database concept:

§ When selecting by field, add the ability to select by regex or like

§ Add save/load functionality

§ Turn the database into a client/server model

§ Integrate error handling (most of it is done except the data input)

§ Create transactional support (you’ll notice with the immutable database, you already have some of this ready for use)

By completing more examples and forcing yourself to implement features based on functional concepts, you’ll become a better functional programmer.

I’ve watched quite a few people start out programming in imperative style while trying to learn functional programming. At some point—sometimes it’s while covering functional OOP concepts, and sometimes it’s later—it “clicks” for people, and they start seeing code in loops and lists. They start thinking of functions as variables and begin to pass them around instead of just data. When you begin to pass around parts of the application itself, you open up the possibilities of your code and what you can accomplish.