介紹 使用函數式編程來豐富面向對象編程的想法是陳舊的。將函數編程功能添加到面向對象的語言中會帶來面向對象編程設計的好處。 一些舊的和不太老的語言,具有函數式編程和麵向對象的編程: 例如,Smalltalk和Common Lisp。 最近是Python或Ruby。 面向對象編程中模擬的函數式編程技術 ...
介紹
使用函數式編程來豐富面向對象編程的想法是陳舊的。將函數編程功能添加到面向對象的語言中會帶來面向對象編程設計的好處。
一些舊的和不太老的語言,具有函數式編程和麵向對象的編程:
- 例如,Smalltalk和Common Lisp。
- 最近是Python或Ruby。
面向對象編程中模擬的函數式編程技術
面向對象編程語言的實踐包括函數編程技術的模擬:
- C ++:函數指針和()運算符的重載。
- Java:匿名類和反思。
粒度不匹配
功能編程和麵向對象編程在不同的設計粒度級別上運行:
- 功能/方法:在小程度上編程。
- 類/對象/模塊:大規模編程。
Threre至少有兩個問題:
- 我們在面向對象的編程體繫結構中如何定位各個函數的來源?
- 我們如何將這些單獨的函數與面向對象的編程體繫結構聯繫起來?
面向對象的函數式編程構造
C#提供了一個名為delegates的函數編程功能:
delegate string StringFunType(string s); // declaration
string G1(string s){ // a method whose type matches StringFunType
return "some string" + s;
}
StringFunType f1; // declaration of a delegate variable
f1 = G1; // direct method value assignment
f1("some string"); // application of the delegate variable
代表是一流的價值觀。這意味著委托類型可以鍵入方法參數,並且委托可以作為任何其他值的參數傳遞:
string Gf1(StringFunType f, string s){ [ ... ] } // delegate f as a parameter
Console.WriteLine(Gf1(G1, "Boo")); // call
代理可以作為方法的計算返回。例如,假設G是一個string => string類型的方法,併在SomeClass中實現:
StringFunType Gf2(){ // delegate as a return value
[ ... ]
return (new SomeClass()).G;
}
Console.WriteLine(Gf2()("Boo")); // call
代表可以進入數據結構:
var l = new LinkedList<StringFunType>(); // list of delegates
[ ... ]
l.AddFirst(G1) ; // insertion of a delegate in the list
Console.WriteLine(l.First.Value("Boo")); // extract and call
C#代表可能是匿名的:
delegate(string s){ return s + "some string"; };
匿名委托看起來更像lambda表達式:
s => { return s + "some string"; };
s => s + "some string";
相互關係函數式編程/面向對象程式設計
擴展方法使程式員能夠在不創建新派生類的情況下向現有類添加方法:
static int SimpleWordCount(this string str){
return str.Split(new char[]{' '}).Length;
}
string s1 = "some chain";
s1.SimpleWordCount(); // usable as a String method
SimpleWordCount(s1); // also usable as a standalone method
擴展方法的另一個例子:
static IEnumerable<T> MySort<T>(this IEnumerable<T> obj) where T:IComparable<T>{
[ ... ]
}
List<int> someList = [ ... ];
someList.MySort();
擴展方法在C#中有嚴格的限制:
- 只有靜態
- 不是多態的
C#中的函數式編程集成
C#為arity提供功能和程式通用委托預定義類型,最多16個:
delegate TResult Func<TResult>();
delegate TResult Func<T, TResult>(T a1);
delegate TResult Func<T1, T2, TResult>(T1 a1, T2 a2);
delegate void Action<T>(T a1);
[ ... ]
委托本身可以包含委托的調用列表。調用此委托時,委托中包含的方法將按它們在列表中出現的順序調用。結果值由列表中調用的最後一個方法確定。
C#允許將lambda表達式表示為稱為表達式樹的數據結構:
Expression<Func<int, int>> expression = x => x + 1;
var d = expression.Compile();
d.Invoke(2);
因此,它們可以被存儲和傳輸。
功能級別的代碼抽象
一個簡單的代碼:
float M(int y){
int x1 = [ ... ];
int x2 = [ ... ];
[ ... ]
[ ... some code ... ]; // some code using x1, x2 and y
[ ... ]
}
功能抽象:
public delegate int Fun(int x, int y, int z);
float MFun(Fun f, int x2, int y){
int x1 = [ ... ];
[ ... ]
f(x1, x2, y);
[ ... ]
}
int z1 = MFun(F1, 1, 2);
int z2 = MFun(F2, 1, 2);
功能抽象的優點是沒有局部重覆,並且存在關註點分離。
功能抽象的簡單有效應用是對數據的通用高階迭代操作。
例如,內部迭代器(Maps):
IEnumerable<T2> Map<T1, T2>(this IEnumerable<T1> data, Func<T1, T2> f){
foreach(var x in data)
yield return f(x);
}
someList.Map(i => i * i);
運營組成
在功能編程中,操作組合物很容易。初始代碼:
public static void PrintWordCount(string s){
string[] words = s.Split(' ');
for(int i = 0; i < words.Length; i++)
words[i] = words[i].ToLower();
var dict = new Dictionary<string, int>();
foreach(var word in words)
if (dict.ContainsKey(word))
dict[word]++;
else
dict.Add(word, 1);
foreach(var x in dict)
Console.WriteLine("{0}: {1}", x.Key, x.Value.ToString());
}
使用高階函數的第一個因數
public static void PrintWordCount(string s){
string[] words = s.Split(' ');
string[] words2 = (string[]) Map(words, w => w.ToLower());
Dictionary<string, int> res = (Dictionary<string, int>) Count(words2);
App(res, x => Console.WriteLine("{0}: {1}", x.Key, x.Value.ToString()));
}
使用擴展方法的第二個因數:
public static void PrintWordCount(string s){
s
.Split(' ')
.Map(w => w.ToLower())
.Count()
.App(x => Console.WriteLine("{0}: {1}", x.Key, x.Value.ToString()));
}
我們可以看到代碼的可讀性增加了。
在C#中,這種操作組合通常與LINQ一起使用,LINQ被定義為將編程與關係數據或XML統一起來。下麵是一個使用LINQ的簡單示例:
var q = programmers
.Where(p => p.Age > 20)
.OrderByDescending(p => p.Age)
.GroupBy(p => p.Language)
.Select(g => new { Language = g.Key, Size = g.Count(), Names = g });
功能部分應用和Currying
使用第一類函數,每個n元函數都可以轉換為n個一元函數的組合,即成為一個curried函數:
Func<int, int, int> lam1 = (x, y) => x + y;
Func<int, Func<int, int>> lam2 = x => (y => x + y);
Func<int, int> lam3 = lam2(3) ; // partial application
柯里:
public static Func<T1, Func<T2, TRes>> Curry<T1, T2, TRes>(this Func<T1, T2, TRes> f){
return (x => (y => f(x, y)));
}
Func<int, int> lam4 = lam1.Curry()(3); // partial application
面向對象編程中的體繫結構功能編程技術
在面向對象編程中具有函數編程功能的一些架構效果:
- 減少對象/類定義的數量。
- 在函數/方法級別命名抽象。
- 操作組合(和序列理解)。
- 功能部分應用和currying。
一些經典的面向對象設計模式與功能編程
為什麼函數式編程通常集成到面向對象的編程中?
主要的面向對象編程語言基於類作為模塊:C#,C ++,Java。
面向對象編程中開發的強大思想之一:維護,擴展和適應操作可以通過繼承和類組合(這避免了對現有代碼的任何修改)。函數式編程源碼是這個問題的解決方案。
例如,戰略設計模式。
戰略
策略模式允許演算法獨立於使用它的客戶端而變化。
策略:只是在方法級別抽象代碼的情況(不需要面向對象的封裝和新的類層次結構)。例如,在.NET Framework中:
public delegate int Comparison<T>(T x, T y);
public void Sort(Comparison<T> comparison);
public delegate bool Predicate<T>(T obj);
public List<T> FindAll(Predicate<T> match);
其他設計模式,如命令,觀察者,訪問者和虛擬代理,可以使一流的功能受益:
命令
Command模式將請求(方法調用)封裝為對象,以便可以輕鬆地傳輸,存儲和應用它們。例如,菜單實現:
public delegate void EventHandler(object sender, EventArgs e);
public event EventHandler Click;
private void menuItem1_Click(object sender, EventArgs e){
OpenFileDialog fd = new OpenFileDialog();
fd.DefaultExt = "*.*" ;
fd.ShowDialog();
}
public void CreateMyMenu(){
MainMenu mainMenu1 = new MainMenu();
MenuItem menuItem1 = new MenuItem();
[ ... ]
menuItem1.Click += new EventHandler(menuItem1_Click);
}
觀察
對象之間的一對多依賴關係,以便當一個對象更改狀態時,將通知並更新其所有依賴項。
下麵是觀察者設計模式的經典實現:
public interface Observer<S>{
void Update(S s);
}
public abstract class Subject<S>{
private List<Observer<S>> _observ = new List<Observer<S>>();
public void Attach(Observer<S> obs){
_observ.Add(obs);
}
public void Notify(S s){
foreach (var obs in _observ)
obs.Update(s);
}
}
功能編程:
public delegate void UpdateFun<S>(S s);
public abstract class Subject<S>{
private UpdateFun<S> _updateHandler;
public void Attach(UpdateFun<S> f){
_updateHandler += f;
}
public void Notify(S s){
_updateHandler(s);
}
}
我們可以看到,不需要使用名為Update的方法的觀察者類。
虛擬代理
虛擬代理模式:其他對象的占位符,以便僅在需要時創建/計算其數據。
下麵是虛擬代理設計模式的經典實現:
public class SimpleProxy : I{
private Simple _simple;
private int _arg;
protected Simple GetSimple(){
if (_simple == null)
_simple = new Simple(_arg);
return _simple;
}
public SimpleProxy(int i){
_arg = i ;
}
public void Process(){
GetSimple().Process();
}
}
下麵使用函數式編程和懶惰實現虛擬代理設計模式:
public class SimpleLazyProxy : I{
private Lazy<Simple> _simpleLazy;
public SimpleLazyProxy(int i){
_simpleLazy = new Lazy<Simple>(() => new Simple(i));
}
public void Process(){
_simpleLazy.Value.Process();
}
}
游客
訪問者模式允許您定義新操作,而無需更改其操作元素的類。如果沒有訪問者,則必須單獨編輯或派生層次結構的每個子類。訪客是許多編程設計問題的關鍵。
以下是訪問者設計模式的經典實現:
public interface IFigure{
string GetName();
void Accept<T>(IFigureVisitor<T> v);
}
public class SimpleFigure : IFigure{
private string _name;
public SimpleFigure(string name){
_name = name;
}
public string GetName(){
return _name;
}
public void Accept<T>(IFigureVisitor<T> v){
v.Visit(this);
}
}
public class CompositeFigure : IFigure{
private string _name;
private IFigure[] _figureArray;
public CompositeFigure(string name, IFigure[] s){
_name = name;
_figureArray = s;
}
public string GetName(){
return _name;
}
public void Accept<T>(IFigureVisitor<T> v){
foreach (IFigure f in _figureArray)
f.Accept (v);
v.Visit(this);
}
}
public interface IFigureVisitor<T>{
T GetVisitorState();
void Visit(SimpleFigure f);
void Visit(CompositeFigure f);
}
public class NameFigureVisitor : IFigureVisitor<string>{
private string _fullName = " ";
public string GetVisitorState(){
return _fullName;
}
public void Visit(SimpleFigure f){
_fullName += f.GetName() + " ";
}
public void Visit(CompositeFigure f){
_fullName += f.GetName() + "/";
}
}
訪客的一些眾所周知的弱點:
- 重構阻力:訪客定義取決於其運行的客戶端源碼類集。
- 靜態:訪問者的實現是靜態的(類型安全但靈活性較低)。
- 入侵:訪問者需要客戶類預期和/或參與選擇正確的方法。
- 命名不靈活:訪問者需要同樣命名訪問方法的所有不同實現。
嘗試使用擴展方法解決訪問者問題:
public interface IFigure{
string GetName(); // no Accept method required
}
[ ... ]
public static class NameFigureVisitor{
public static void NameVisit(this SimpleFigure f){
_state = f.GetName() + " " + _state;
}
public static void NameVisit(this CompositeFigure f) {
_fullName = f.GetName() + ":" + _fullName;
foreach(IFigure g in f.GetFigureArray())
g.NameVisit(); // dynamic dispatch required...
[ ... ]
}
}
通過函數式編程,Visitors可以是函數:
public delegate T VisitorFun<V, T>(V f);
public interface IFigureF{
string GetName ();
T Accept<T>(VisitorFun<IFigureF, T> v);
}
public class SimpleFigureF : IFigureF{
private string _name ;
public SimpleFigureF(string name){
_name = name ;
}
public string GetName(){
return _name ;
}
public T Accept<T>(VisitorFun<IFigureF, T> v){
return v(this);
}
}
[...]
public class CompositeFigureF : IFigureF{
private string _name;
private IFigureF[ ] _figureArray;
public CompositeFigureF(string name, IFigureF[] s){
_name = name;
_figureArray = s;
}
public string GetName(){
return this._name;
}
public T Accept<T>(VisitorFun<IFigureF, T> v){
foreach(IFigureF f in _figureArray)
f.Accept(v);
return v(this);
}
}
以下簡單功能訪客:
public static VisitorFun<IFigureF, string> MakeNameFigureVisitorFun(){
string _state = "";
return obj => {
if(obj is SimpleFigureF)
_state += obj.GetName() + " ";
else if(obj is CompositeFigureF)
_state += obj.GetName() + "/";
return _state ;
};
}
以數據為導向的訪問者:
var dict1 = new Dictionary<Type, VisitorFun<IFigureF, string>>();
dict1.Add(typeof(SimpleFigureF), f => f.GetName() + " ");
dict1.Add(typeof(CompositeFigureF), f => f.GetName() + "/");
var nameFigureFunVisitor1 = MakeVisitorFun<IFigureF, string>(dict1);
我們可以看到,通過功能編程和數據驅動編程,重構阻力更小,名稱剛性更小,靜態更少。
加起來
具有函數式編程粒度級別的對象 - 節點編程:
- 函數式編程適用於模塊化對象。
- 函數/方法級別的代碼抽象。
- 方便的通用迭代器/迴圈實現。
- 操作組合,序列/查詢理解。
- 功能部分應用。
- 對象/類定義數量的限制。
- 在函數/方法級別命名抽象。
- 懶惰模擬(在虛擬代理中使用)。
- 數據驅動編程(在訪客中使用)。
- 架構簡化。
- 增加靈活性。
將函數編程功能添加到面向對象的語言中會帶來面向對象編程設計的好處。