What Will I Learn?
This tutorial explains Functional Programming to lazy programmers: FP enables you to circle verbs.
Requirements
The F# Compiler, Core Library & Tools http://fsharp.org
Difficulty
Intermediate
Tutorial Contents
- The killer example
- What makes the killer
Functional Programming to lazy programmers
I am a lazy programmer. I just start to get comfortable with C# and object-oriented stuff: encapsulation, inheritance, polymorphism, etc. Here comes the new buzz words, F# or functional programming (FP). Do I have to start from scratch to learn a new language all over again? Will I be irrelevant in 2 or 3 years if I am not catching up? Exactly what is the fuzz about FP?
After several hours of googling and visiting dozens of FP introduction websites, I was still in the dark. Some of the websites threw out tons of computer science jargons intended for those who already knew what FP is all about. The others quickly jumped into "Hello World" examples, introducing the syntax, not the concept, to those who have already decided to learn the new language. As a lazy programmer currently comfortable and productive with OO stuff, I am not sold on FP just yet. I am glad I have not wasted much time on the dynamic language frantic, such as Iron Ruby or Iron Python, which is short- lived. Will FP be a different story? I need something to convince me that FP is a true game changer, not another pretender. Especially, I would like to see a simple yet practical example that demonstrates the TRUE, not syntactic,difference between FP and OO programming.
The killer example
Here is a simple class with several properties.
public class MyOOClass {
public int PropertyA { get; set; }
public string PropertyB { get; set; }
public DateTime PropertyC { get; set; }
}
The task is to write an overloaded method to load an instance from various data sources, such as DataTable
filled by DataAdapter
, DataReader
resulted from DbCommand.ExcuteReader
(), or property/value pairs read from configuration files. Below are the signatures of the method overloads.
public class MyOOClass {
public int PropertyA { get; set; }
public string PropertyB { get; set; }
public DateTime PropertyC { get; set; }
}
Piece of cake. Here we go.
public static MyOOClass LoadInstance(DataRow dataSrc) {
return new MyOOClass() {
PropertyA = (int)dataSrc["PropertyA"],
PropertyB = (string)dataSrc["PropertyB"],
PropertyC = (DateTime)dataSrc["PropertyC"]
};
}
public static MyOOClass LoadInstance(IDataReader dataSrc) {
return new MyOOClass() {
PropertyA = (int)dataSrc["PropertyA"],
PropertyB = (string)dataSrc["PropertyB"],
PropertyC = (DateTime)dataSrc["PropertyC"]
};
}
public static MyOOClass LoadInstance(IDictionary dataSrc) {
return new MyOOClass() {
PropertyA = (int)dataSrc["PropertyA"],
PropertyB = (string)dataSrc["PropertyB"],
PropertyC = (DateTime)dataSrc["PropertyC"]
};
}
Wait a minute. Something is fishy here. The method bodies are exactly the same! That's a violation of the DRY (Don't Repeat Yourself) or DIE (Duplication is Evil) principle. Duplicated code is a maintenance headache. If, for example, a new property was added to the class, all three overloaded methods would have to be modified. The code should be refactored so that such requirement changes result in code changes in only one place. Upon closer examination, however, it turns out there is no straightforward OO way to refactor the code, because the three versions of dataSrc
parameter do not share a common superclass or interface that implements the string-keyed indexer ([string]). Work-around is possible. For example, use a wrapper class to force them using the same indexer.
public static MyOOClass LoadInstance(DataRow dataSrc) {
return load(new Wrapper(dataSrc));
}
public static MyOOClass LoadInstance(IDataReader dataSrc) {
return load(new Wrapper(dataSrc));
}
public static MyOOClass LoadInstance(IDictionary dataSrc) {
return load(new Wrapper(dataSrc));
}
private class Wrapper {
private object wrapee;
public Wrapper(DataRow dataSrc) {
wrapee = dataSrc;
}
public Wrapper(IDataReader dataSrc) {
wrapee = dataSrc;
}
public Wrapper(IDictionary dataSrc) {
wrapee = dataSrc;
}
public object this[string key]{
get {
if (wrapee is DataRow) {
return (wrapee as DataRow)[key];
} else if (wrapee is IDataReader) {
return (wrapee as IDataReader)[key];
} else if (wrapee is IDictionary) {
return (wrapee as IDictionary)[key];
} else
throw new Exception("Unsupported source type.");
}
}
}
private static MyOOClass load(Wrapper dataSrc) {
return new MyOOClass() {
PropertyA = (int)dataSrc["PropertyA"],
PropertyB = (string)dataSrc["PropertyB"],
PropertyC = (DateTime)dataSrc["PropertyC"]
};
}
The wrapper removes duplicated code from LoadInstance
methods. However, it is anything but pretty. It doubles lines of code. This is like a Chinese proverb saying, "pick up a sesame seed but lose sight of a watermelon" (focus on details but miss the big picture). The big picture here is to reduce maintenance cost by avoiding code duplication. Increasing code size adds more code to maintain, defeating the big picture in the first place. In fact, it does not eliminate the duplication. It just moves the duplication from LoadInstance
methods to the wrapper class. In some cases, the wrapper may actually introduce more code changes. For example, in order to load instances from a new type of data source, the original version needs only another overload, while the wrapper version needs additional code to wrap the new data type.Another work-around, which avoids code duplication without increasing code size, is to use Reflection to invoke the indexer from the common super class (object
), as shown below.
public static MyOOClass LoadInstance
(DataRow dataSrc) {
return load(dataSrc);
}
public static MyOOClass LoadInstance(IDataReader dataSrc) {
return load(dataSrc);
}
public static MyOOClass LoadInstance(IDictionary dataSrc) {
return load(dataSrc);
}
private static MyOOClass load(object dataSrc) {
MethodInfo indexer =
dataSrc.GetType().GetMethod("get_Item", new Type[] { typeof(string) });
return new MyOOClass() {
PropertyA = (int)(indexer.Invoke(dataSrc, new object[] { "PropertyA" })),
PropertyB = (string)(indexer.Invoke(dataSrc, new object[] { "PropertyB" })),
PropertyC = (DateTime)(indexer.Invoke(dataSrc, new object[] { "PropertyC" }))
};
}
Boy is it uglier! Albeit shorter, the code is much harder to read. Hard-to-read code is also difficult for maintenance. More importantly, it is magnitude slower. This is like another Chinese proverb saying, "cut the feet to fit the shoes." You put on shoes in order to protect your feet. But your action actually does more harm than good.So what has FP to offer?
public static MyFPClass LoadInstance(DataRow dr) {
return load(propName => dr[propName]);
}
public static MyFPClass LoadInstance(IDataReader dr) {
return load(propName => dr[propName]);
}
public static MyFPClass LoadInstance(IDictionary dr) {
return load(propName => dr[propName]);
}
private static MyFPClass load(Func<string, object> getVal) {
return new MyFPClass() {
PropertyA = (int)getVal("PropertyA"),
PropertyB = (string)getVal("PropertyB"),
PropertyC = (DateTime)getVal("PropertyC")
};
}
I call this elegancy.
Note
Yes, FP is available in your favorite language, C# or VB. It is not specific to F#, so you don't have to start from scratch. In fact, you can do FP even in .Net using delegates. Since .Net, FP becomes "native" after a new generic delegate (Func) is predefined, and a new syntax for anonymous delegates is introduced.
What makes the killer
The example is carefully selected, so that the refactored LoadInstance
method does not care the actual object of dataSrc
. It only needs to know the behavior, or function, by which the dataSrc
returns an object given a string-valued key. In OO paradigm, however, you cannot pass a "pure" function to a method. All functions are attached to objects. There are no stand-alone pure functions. Although you can define behaviors using function-only interfaces and pass them as parameters, they are objects’behaviors, not pure behaviors. You cannot instantiate or pass a pure interface. Any interface is eventually implemented through a concrete object. In this example, the need for a pure function as method parameter makes both OO implementations looking awkward.
In FP paradigm, functions become first-class citizens as objects. In addition to their traditional roles as pieces of reusable code defined at design time, functions can now be constructed and manipulated at runtime. They can also be passed around as parameters. In other words, functions are abstracted to the same level as objects. While objects enable you to encapsulate entities with internal states and behaviors, functions give you the ability to model pure behaviors not attached any particular entities. More precisely, model behaviors of generic objects, or generic behaviors. In this case, FP enables us to model the generic behavior of a string indexer (getVal
) in the load
method; and lambda expression enables us to single out and pass just the string indexers from different types of dataSrc
. This gives the elegancy to the FP implementation.
The ability to model behaviors is a true game changer, not a syntactic sugar. Just look what changes LINQ has brought along. LINQ completely changes the way you think about data manipulation. Before LINQ, you code how to manipulate data. With LINQ, you code what data you need. It is not an accident that most LINQ keywords are not nouns, such as select
, where
, group by
, count
, distinct
, etc. It is only because they are modeling behaviors, not entities.
Next time when you are given a requirement statement to architect a new project, in addition to nouns, you could also circle those recurring verbs, adverbs, or even conjunctions. FP enables you to model generic behaviors and bring your project to the next level.
Posted on Utopian.io - Rewarding Open Source Contributors
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
Hey @haig I am @utopian-io. I have just upvoted you!
Achievements
Suggestions
Get Noticed!
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit