From object to function composition
Composition is a technique often used to build software by creating cohesive units that interact with each other to form the overall system. At the code level these units mainly consist of functions and objects. A system can be built by composing objects with abstractions that define the rules for communication with other objects. An alternative way of building the system is by composing functions and defining the rules of communication through functional contracts (i.e. passing functions into functions). Coming from an object oriented background I feel this second approach is worth exploring further.
Using the C# GildedRose kata as an example, an object oriented way of expressing the quality adjustment in a product looks like this
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class GildedRose | |
{ | |
private static readonly Dictionary<string, IProduct> Products = | |
new Dictionary<string, IProduct> | |
{ | |
{"Aged Brie", new Product(new QualityAdjuster(+1))}, | |
{"Backstage passes to a TAFKAL80ETC concert", | |
new Product(new BackstageQualityAdjuster(1))}, | |
{"Sulfuras, Hand of Ragnaros", new SulfurasProduct()}, | |
{"NORMAL ITEM", new Product(new QualityAdjuster(-1))}, | |
{"Conjured Mana Cake", new Product(new QualityAdjuster(0))} | |
}; | |
public static void UpdateQuality(List<Item> items) | |
{ | |
foreach (Item item in items) | |
Products[item.Name].Update(item); | |
} | |
} | |
public interface IProduct | |
{ | |
void Update(Item item); | |
} | |
public class Product : IProduct | |
{ | |
private readonly IAdjustQuality _qualityAdjuster; | |
public Product(IAdjustQuality qualityAdjuster) | |
{ | |
_qualityAdjuster = qualityAdjuster; | |
} | |
public void Update(Item item) | |
{ | |
item.SellIn = item.SellIn - 1; | |
item.Quality = item.Quality + (_qualityAdjuster.Adjust(item)); | |
item.Quality = Math.Min(50, item.Quality); | |
item.Quality = Math.Max(0, item.Quality); | |
} | |
} | |
public class SulfurasProduct : IProduct | |
{ | |
public void Update(Item item) | |
{ | |
} | |
} | |
public interface IAdjustQuality | |
{ | |
int Adjust(Item item); | |
} | |
public class QualityAdjuster : IAdjustQuality | |
{ | |
private readonly int _defaultQuality; | |
public QualityAdjuster(int defaultQuality) | |
{ | |
_defaultQuality = defaultQuality; | |
} | |
public int Adjust(Item item) | |
{ | |
return item.SellIn < 0 ? 2 * _defaultQuality : _defaultQuality; | |
} | |
} | |
public class BackstageQualityAdjuster : IAdjustQuality | |
{ | |
private readonly int _defaultQuality; | |
public BackstageQualityAdjuster(int defaultQuality) | |
{ | |
_defaultQuality = defaultQuality; | |
} | |
public int Adjust(Item item) | |
{ | |
if (item.SellIn < 0) | |
return -item.Quality; | |
if (item.SellIn < 5) | |
return +3; | |
if (item.SellIn < 10) | |
return +2; | |
return _defaultQuality; | |
} | |
} |
An alternative way to express the quality adjustment in a Product in the GildedRose example would be to compose functions rather than objects. Although C# traditionally has been perceived to be an object oriented language but with lambda expressions it is very much possible to write functional code in C#. Single-method interfaces are easy to replace with the usage of closures and "function pointers" as long as the function has a compatible signature to the method declared in the single-method interface. Lets see how we can replace IAdjustQuality with its functional equivalent.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class GildedRose | |
{ | |
private static readonly Func<int, Func<Item, int>> AdjustQuality = | |
quality => item => item.SellIn < 0 ? 2*quality : quality; | |
private static readonly Func<Item, int> AdjustBackstageQuality = | |
item => | |
{ | |
if (item.SellIn < 0) | |
return -item.Quality; | |
if (item.SellIn < 5) | |
return +3; | |
if (item.SellIn < 10) | |
return +2; | |
return 1; | |
}; | |
private static readonly Dictionary<string, IProduct> Products = | |
new Dictionary<string, IProduct> | |
{ | |
{"Aged Brie", new Product(AdjustQuality(1))}, | |
{"Backstage passes to a TAFKAL80ETC concert", | |
new Product(AdjustBackstageQuality)}, | |
{"Sulfuras, Hand of Ragnaros", new SulfurasProduct()}, | |
{"NORMAL ITEM", new Product(AdjustQuality(-1))}, | |
{"Conjured Mana Cake", new Product(AdjustQuality(0))} | |
}; | |
public static void UpdateQuality(List<Item> items) | |
{ | |
foreach (Item item in items) | |
Products[item.Name].Update(item); | |
} | |
} |
Creating the AdjustQuality and AdjustBackstageQuality functions results in the removal of 2 classes and the interface declaration itself. Notice AdjustQuality function uses partial application i.e. it is a function that returns another function. The first function is applied partially to return the actual function. This should be more clear after looking at the Product class now.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Product : IProduct | |
{ | |
private readonly Func<Item, int> _adjustQuality; | |
public Product(Func<Item, int> adjustQuality) | |
{ | |
_adjustQuality = adjustQuality; | |
} | |
public void Update(Item item) | |
{ | |
item.SellIn = item.SellIn - 1; | |
item.Quality = item.Quality + (_adjustQuality(item)); | |
item.Quality = Math.Min(50, item.Quality); | |
item.Quality = Math.Max(0, item.Quality); | |
} | |
} |
Applying the same technique of replacing an interface with its equivalent function pointer the IProduct interface can also be removed and the resulting code looks like this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class GildedRose | |
{ | |
private static readonly Func<int, Func<Item, int>> AdjustQuality = | |
quality => item => item.SellIn < 0 ? 2 * quality : quality; | |
private static readonly Func<Func<Item, int>, Action<Item>> UpdateProduct = | |
adjustQuality => (item => | |
{ | |
item.SellIn = item.SellIn - 1; | |
item.Quality = item.Quality + (adjustQuality(item)); | |
item.Quality = Math.Min(50, item.Quality); | |
item.Quality = Math.Max(0, item.Quality); | |
}); | |
private static readonly Func<Item, int> AdjustBackstageQuality = item => | |
{ | |
if (item.SellIn < 0) | |
return -item.Quality; | |
if (item.SellIn < 5) | |
return +3; | |
if (item.SellIn < 10) | |
return +2; | |
return 1; | |
}; | |
private static readonly Dictionary<string, Action<Item>> Products = | |
new Dictionary<string, Action<Item>> | |
{ | |
{"Aged Brie", UpdateProduct(AdjustQuality(1))}, | |
{"Backstage passes to a TAFKAL80ETC concert", | |
UpdateProduct(AdjustBackstageQuality)}, | |
{"Sulfuras, Hand of Ragnaros", item => { }}, | |
{"NORMAL ITEM", UpdateProduct(AdjustQuality(-1))}, | |
{"Conjured Mana Cake", UpdateProduct(AdjustQuality(0))} | |
}; | |
public static void UpdateQuality(List<Item> items) | |
{ | |
foreach (Item item in items) | |
Products[item.Name](item); | |
} | |
} |