1 public interface IUnit 2 { 3 void ChangeState(UnitStateEnum state); 4 void Patrol(); 5 IUnit GetNearestTarget(); 6 void LockTarget(IUnit unit); 7 float GetFleeBloodRate(); 8 bool CanMove(); 9 bool HpRateLessThan(float rate); 10 void Flee(); 11 void Speak(); 12 }然後,我們可以通過一個簡單的有限狀態機(FSM)來控制這個單位的行為。不同狀態下,單位都具有不同的行為准則,以形成智能體。 具體來說,我們可以定義這樣幾種狀態:
1 public interface IState<TState, TUnit> where TState : IConvertible 2 { 3 TState Enum { get; } 4 TUnit Self { get; } 5 void OnEnter(); 6 void Drive(); 7 void OnExit(); 8 }
以逃跑狀態為例:
1 public class FleeState : UnitStateBase 2 { 3 public FleeState(IUnit self) : base(UnitStateEnum.Flee, self) 4 { 5 } 6 public override void OnEnter() 7 { 8 Self.Flee(); 9 } 10 public override void Drive() 11 { 12 var unit = Self.GetNearestTarget(); 13 if (unit != null) 14 { 15 return; 16 } 17 18 Self.ChangeState(UnitStateEnum.Patrol); 19 } 20 }
1 public interface IState<TState, TUnit> where TState : IConvertible 2 { 3 TState Enum { get; } 4 void OnEnter(TUnit self); 5 void Drive(TUnit self); 6 void OnExit(TUnit self); 7 }
還是拿之前實現好的逃跑狀態作為例子:
1 public class FleeState : UnitStateBase 2 { 3 public FleeState() : base(UnitStateEnum.Flee) 4 { 5 } 6 public override void OnEnter(IUnit self) 7 { 8 base.OnEnter(self); 9 self.Flee(); 10 } 11 public override void Drive(IUnit self) 12 { 13 base.Drive(self); 14 15 var unit = self.GetNearestTarget(); 16 if (unit != null) 17 { 18 return; 19 } 20 21 self.ChangeState(UnitStateEnum.Patrol); 22 } 23 }這樣,就區分了動態與靜態。靜態的是狀態之間的遷移邏輯,只要不做熱更新,是不會變的結構。動態的是狀態遷移過程中的上下文,根據不同的上下文來決定。
考慮這樣一個組合狀態情景:巡邏時,需要依次得先走到一個點,然後怠工一會兒,再走到下一個點,然後再怠工一會兒,循環往復。這樣就需要父狀態(巡邏狀態)注記當前激活的子狀態,並且根據子狀態執行結果的不同來修改激活的子狀態集合。這樣不僅是Unit自身有上下文,連組合狀態也有了自己的上下文。
為了簡化討論,我們還是從non-ContextFree層次狀態機系統設計開始。
修改後的狀態定義:1 public interface IState<TState, TCleverUnit, TResult> 2 where TState : IConvertible 3 { 4 // ... 5 TResult Drive(); 6 // ... 7 }
組合狀態的定義:
1 public abstract class UnitCompositeStateBase : UnitStateBase 2 { 3 protected readonly LinkedList<UnitStateBase> subStates = new LinkedList<UnitStateBase>(); 4 5 // ... 6 protected Result ProcessSubStates() 7 { 8 if (subStates.Count == 0) 9 { 10 return Result.Success; 11 } 12 13 var front = subStates.First; 14 var res = front.Value.Drive(); 15 16 if (res != Result.Continue) 17 { 18 subStates.RemoveFirst(); 19 } 20 21 return Result.Continue; 22 } 23 // ... 24 } 25巡邏狀態現在是一個組合狀態:
1 public class PatrolState : UnitCompositeStateBase 2 { 3 // ... 4 public override void OnEnter() 5 { 6 base.OnEnter(); 7 AddSubState(new MoveToState(Self)); 8 } 9 10 public override Result Drive() 11 { 12 if (subStates.Count == 0) 13 { 14 return Result.Success; 15 } 16 17 var unit = Self.GetNearestTarget(); 18 if (unit != null) 19 { 20 Self.LockTarget(unit); 21 return Result.Success; 22 } 23 24 var front = subStates.First; 25 var ret = front.Value.Drive(); 26 27 if (ret != Result.Continue) 28 { 29 if (front.Value.Enum == CleverUnitStateEnum.MoveTo) 30 { 31 AddSubState(new IdleState(Self)); 32 } 33 else 34 { 35 AddSubState(new MoveToState(Self)); 36 } 37 } 38 39 return Result.Continue; 40 } 41 }看過《游戲人工智能編程精粹》的同學可能看到這裡就會發現,這種層次狀態機其實就是這本書裡講的目標驅動的狀態機。組合狀態就是組合目標,子狀態就是子目標。父目標/狀態的調度取決於子目標/狀態的完成情況。這種狀態框架與普通的trivial狀態機模型的區別僅僅是增加了對層次狀態的支持,狀態的遷移還是需要靠顯式的ChangeState來做。 這本書裡面的狀態框架,每個狀態的執行status記錄在了實例內部,不方便後續的優化,我們這裡實現的時候首先把這個做成純驅動式的。但是還不夠。現在之前的ContextFree優化成果已經回退掉了,我們還需要補充回來。
1 public class Continuation 2 { 3 public Continuation SubContinuation { get; set; } 4 public int NextStep { get; set; } 5 public object Param { get; set; } 6 } 7 8 public class Context<T> 9 { 10 public Continuation Continuation { get; set; } 11 public T Self { get; set; } 12 }修改State的接口定義為:
1 public interface IState<TCleverUnit, TResult> 2 { 3 TResult Drive(Context<TCleverUnit> ctx); 4 }已經相當簡潔了。 這樣,我們對之前的巡邏狀態也做下修改,達到一個ContextFree的效果。利用Context中的Continuation來確定當前結點應該從什麼狀態繼續:
1 public class PatrolState : IState<ICleverUnit, Result> 2 { 3 private readonly List<IState<ICleverUnit, Result>> subStates; 4 public PatrolState() 5 { 6 subStates = new List<IState<ICleverUnit, Result>>() 7 { 8 new MoveToState(), 9 new IdleState(), 10 }; 11 } 12 public Result Drive(Context<ICleverUnit> ctx) 13 { 14 var unit = ctx.Self.GetNearestTarget(); 15 if (unit != null) 16 { 17 ctx.Self.LockTarget(unit); 18 19 return Result.Success; 20 } 21 22 var nextStep = 0; 23 if (ctx.Continuation != null) 24 { 25 // Continuation 26 var thisContinuation = ctx.Continuation; 27 28 ctx.Continuation = thisContinuation.SubContinuation; 29 30 var ret = subStates[nextStep].Drive(ctx); 31 32 if (ret == Result.Continue) 33 { 34 thisContinuation.SubContinuation = ctx.Continuation; 35 ctx.Continuation = thisContinuation; 36 37 return Result.Continue; 38 } 39 else if (ret == Result.Failure) 40 { 41 ctx.Continuation = null; 42 43 return Result.Failure; 44 } 45 46 ctx.Continuation = null; 47 nextStep = thisContinuation.NextStep + 1; 48 } 49 50 for (; nextStep < subStates.Count; nextStep++) 51 { 52 var ret = subStates[nextStep].Drive(ctx); 53 if (ret == Result.Continue) 54 { 55 ctx.Continuation = new Continuation() 56 { 57 SubContinuation = ctx.Continuation, 58 NextStep = nextStep, 59 }; 60 61 return Result.Continue; 62 } 63 else if (ret == Result.Failure) 64 { 65 ctx.Continuation = null; 66 67 return Result.Failure; 68 } 69 } 70 71 ctx.Continuation = null; 72 73 return Result.Success; 74 } 75 }
subStates是readonly的,在組合狀態構造的一開始就確定了值。這樣結構本身就是靜態的,而上下文是動態的。不同的entity instance共用同一個樹的instance。
1 public interface IO<T> 2 { 3 T Drive(Context ctx); 4 }這樣,我們之前的所有結點都應該有IO的concept。 之前提出了Parallel、Sequence、Select、Check這樣幾個語義結點。具體的實現細節就不再細說了,簡單列一下代碼結構:
public class Sequence : IO<Result> { private readonly ICollection<IO<Result>> subTrees; public Sequence(ICollection<IO<Result>> subTrees) { this.subTrees = subTrees; } public Result Drive(Context ctx) { throw new NotImplementedException(); } }
With結點的實現,采用我們之前說的第一種方案:
1 public class With<T, TR> : IO<TR> 2 { 3 // ... 4 public TR Drive(Context ctx) 5 { 6 var thisContinuation = ctx.Continuation; 7 var value = default(T); 8 var skipIoGet = false; 9 10 if (thisContinuation != null) 11 { 12 // Continuation 13 ctx.Continuation = thisContinuation.SubContinuation; 14 15 // 0表示需要繼續ioGet 16 // 1表示需要繼續subTree 17 if (thisContinuation.NextStep == 1) 18 { 19 skipIoGet = true; 20 value = (T) thisContinuation.Param; 21 } 22 } 23 24 if (!skipIoGet) 25 { 26 value = ioGet.Drive(ctx); 27 28 if (ctx.Continuation != null) 29 { 30 // ioGet拋出了Continue 31 if (thisContinuation == null) 32 { 33 thisContinuation = new Continuation() 34 { 35 SubContinuation = ctx.Continuation, 36 NextStep = 0, 37 }; 38 } 39 else 40 { 41 thisContinuation.SubContinuation = ctx.Continuation; 42 thisContinuation.NextStep = 0; 43 } 44 45 ctx.Continuation = thisContinuation; 46 47 return default(TR); 48 } 49 } 50 51 var oldValue = box.SetVal(value); 52 var ret = subTree.Drive(ctx); 53 54 box.SetVal(oldValue); 55 56 if (ctx.Continuation != null) 57 { 58 // subTree拋出了Continue 59 if (thisContinuation == null) 60 { 61 thisContinuation = new Continuation() 62 { 63 SubContinuation = ctx.Continuation, 64 }; 65 } 66 67 ctx.Continuation = thisContinuation; 68 thisContinuation.Param = value; 69 } 70 71 return ret; 72 } 73 }
這樣,我們的層次狀態機就全部組件化了。我們可以用通用的語義結點來組合出任意的子狀態,這些子狀態是不具名的,對構建過程更友好。
具體的代碼例子:
Par( Seq(IsFleeing, ((Box<object> a) => With(a, GetNearestTarget, Check(IsNull(a))))(new Box<object>()), Patrol) ,Seq(IsAttacking, ((Box<float> a) => With(a, GetFleeBloodRate, Check(HpRateLessThan(a))))(new Box<float>())) ,Seq(IsNormal, Loop(Par(((Box<object> a) => With(a, GetNearestTarget, Seq(Check(IsNull(a)), LockTarget(a)))(new Box<object>()), Seq(Seq(Check(ReachCurrentPatrolPoint), MoveToNextPatrolPoiont), Idle))))))
看起來似乎是變得復雜了,原來可能只需要一句new XXXState(),現在卻需要自己用代碼拼接出來一個行為邏輯。但是仔細想一下,改成這樣的描述其實對整個工作流是有好處的。之前的形式完全是硬編碼,而現在,似乎讓我們看到了轉數據驅動的可能性。
#region HpRateLessThan private class MessageHpRateLessThan : IO<bool> { public readonly float p0; public MessageHpRateLessThan(float p0) { this.p0 = p0; } public bool Drive(Context ctx) { return ((T)ctx.Self).HpRateLessThan(p0); } } public static IO<bool> HpRateLessThan(float p0) { return new MessageHpRateLessThan(p0); } #endregion經過包裝的行為結點的代碼都是有規律可循的,所以我們可以比較容易的通過一些代碼生成的機制來做。比如通過反射拿到IUnit定義的接口信息,然後直接在這基礎之上做一下包裝,做出來個行為結點的定義。 現在我們再回憶下討論過的With,構造一個葉子結點的時候,參數不一定是literal value,也有可能是經過Box包裹過的。所以就需要對Boax和literal value抽象出來一個公共的概念,葉子結點/行為結點可以從這個概念中拿到值,而行為結點計算本身的構造也只需要依賴於這個概念。 我們把這個概念命名為Thunk。Thunk包裹一個值或者一個box,而就目前來看,這個Thunk,僅需要提供一個我們可以通過其拿到裡面的值的接口就夠了。
public abstract class Thunk<T> { public abstract T GetUserValue(); }對於常量,我們可以構造一個包裹了常量的thunk;而對於box,其天然就屬於Thunk的concept。 這樣,我們就通過一個Thunk的概念,硬生生把樹中的結點與值分割成了兩個概念。這樣做究竟正確不正確呢? 如果一個行為結點的參數可能有的類型本來就是一些primitive type,或者是外部世界(相對於AI世界)的類型,那肯定是沒問題的。但如果需要支持這樣一種特性:外部世界的函數,返回值是AI世界的某個概念,比如一個樹結點;而我的AI世界,希望的是通過這個外部世界的函數,動態的拿到一個結點,再動態的加到我的樹中,或者再動態的傳給不通的外部世界的函數,應該怎麼做? 對於一顆With子樹(Negate表示對子樹結果取反,Continue仍取Continue):
((Box<IO<Result>> a) => With(a, GetNearestTarget, Negate(a)))(new Box<IO<Result>>())語義需要保證,這顆子樹執行到任意時刻,都需要是ContextFree的。 假設IOGet返回的是一個普通的值,確實是沒問題的。 但是因為Box包裹的可能是任意值,例如,假設IOGet返回的是一個IO,
public abstract class IO<T> : Thunk<IO<T>> { public abstract T Drive(Context ctx); public override IO<T> GetUserValue() { return this; } }
痛點:
(declare (HpRateLessThan :: (Float -> IO Result)) (GetFleeBloodRate :: Float) (IsNull :: (Object -> Bool)) (Idle :: IO Result)) (declare (check :: (Bool -> IO Result)) (loop :: (IO Result -> IO Result)) (par :: (List IO Result -> IO Result)))因為是以Scheme為主要借鑒對象,所以內建的復雜類型實現上本質是一個ADT,當然,有針對list構造專用的語法糖,但是其parse出來拿到的AST中一個list終究還是一個ADT。 直接拿例子來說比較直觀:
(import Prelude) (import BaseAI) (define Root (par [(seq [(check IsFleeing) ((\a (check (IsNull a))) GetNearestTarget)]) (seq [(check IsAttacking) ((\b (HpRateLessThan b)) GetFleeBloodRate)]) (seq [(check IsNormal) (loop (par [((\c (seq [(check (IsNull c)) (LockTarget c)])) GetNearestTarget) (seq [(seq [(check ReachCurrentPatrolPoint) MoveToNextPatrolPoiont]) Idle])]))])]))
可以看到,跟S-Expression沒什麼太大的區別,可能lambda的聲明方式變了下。
然後是詞法分析和語法分析,這裡我選擇的是Haskell的ParseC。一些更傳統的選擇可能是lex+yacc/flex+bison。但是這種兩個工具一起混用學習成本就不用說了,也違背了simple is better的初衷。ParseC使用起來就跟PEG是一樣的,PEG這種形式,是天然的結合了正則與top-down parser。haskell支持的algebraic data types,天然就是用來定義AST結構的,簡單直觀。haskell實現的hindly-miner類型系統,又是讓你寫代碼基本編譯通過就能直接run出正確結果,從一定程度上彌補了PEG天生不適合調試的缺陷。一個haskell的庫就能解決lexical&grammar,實在方便。 先是一些AST結構的預定義:module Common where import qualified Data.Map as Map type Identifier = String type ValEnv = Map.Map Identifier Val type TypeEnv = Map.Map Identifier Type type DecEnv = Map.Map Identifier (String,Dec) data Type = NormalType String | GenericType String Type | AppType [Type] data Dec = DefineDec Pat Exp | ImportDec String | DeclareDec Pat Type | DeclaresDec [Dec] data Exp = ConstExp Val | VarExp Identifier | LambdaExp Pat Exp | AppExp Exp Exp | ADTExp String [Exp] data Val = NilVal | BoolVal Bool | IntVal Integer | FloatVal Float | StringVal String data Pat = VarPat Identifier
我在這裡省去了一些跟這篇文章討論的DSL無關的語言特性,比如Pattern的定義我只保留了VarPat;Value的定義我去掉了ClosureVal,雖然語言本身仍然是支持first class function的。
algebraic data type的一個好處就是清晰易懂,定義起來不過區區二十行,但是我們一看就知道之後輸出的AST會是什麼樣。
haskell的ParseC用起來其實跟PEG是沒有本質區別的,組合子本身是自底向上描述的,而parser也是通過parse小元素的parser來構建parse大元素的parser。
例如,haskell的ParseC庫就有這樣幾個強大的特性:
我們可以先根據這些基本的,封裝出來一些通用combinator。
比如正則規則中的star:
star :: Parser a -> Parser [a] star p = star_p where star_p = try plus_p <|> (return []) plus_p = (:) <$> p <*> star_p
比如plus:
plus :: Parser a -> Parser [a] plus p = plus_p where star_p = try plus_p <|> (return []) <?> "plus_star_p" plus_p = (:) <$> p <*> star_p <?> "plus_plus_p"
基於這些,我們可以做組裝出來一個parse lambda-exp的parser(p_seperate是對char、plus這些的組裝,表示形如a,b,c這樣的由特定字符分隔的序列):
p_lambda_exp :: Parser Exp p_lambda_exp = p_between '(' ')' inner <?> "p_lambda_exp" where inner = make_lambda_exp <$ char '\\' <*> p_seperate (p_parse p_pat) "," <*> p_parse p_exp make_lambda_exp [] e = (LambdaExp NilPat e) make_lambda_exp (p:[]) e = (LambdaExp p e) make_lambda_exp (p:ps) e = (LambdaExp p (make_lambda_exp ps e))
有了所有exp的parser,我們就可以組裝出來一個通用的exp parser:
p_exp :: Parser Exp p_exp = listplus [p_var_exp, p_const_exp, p_lambda_exp, p_app_exp, p_adt_exp, p_list_exp] <?> "p_exp"
其中,listplus是一種具有優先級的lookahead:
listplus :: [Parser a] -> Parser a listplus lst = foldr (<|>) mzero (map try lst)對於parser來說,其輸入是源文件其輸出是AST。具體來說,其實就是parse出一個Dec數組,拿到AST,供後續的pipeline消費。 我們之前舉的AI的例子,parse出來的AST大概是這副模樣:
-- Prelude.bh
Right [DeclaresDec [ DeclareDec (VarPat "seq") (AppType [GenericType "List" (GenericType "IO" (NormalType "Result")),GenericType "IO" (NormalType "Result")]) ,DeclareDec (VarPat "check") (AppType [NormalType "Bool",GenericType "IO" (NormalType "Result")])]]
-- BaseAI.bh Right [DeclaresDec [ DeclareDec (VarPat "HpRateLessThan") (AppType [NormalType "Float",GenericType "IO" (NormalType "Result")]) ,DeclareDec (VarPat "Idle") (GenericType "IO" (NormalType "Result"))]]
-- AI00001.bh Right [ ImportDec "Prelude" ,ImportDec "BaseAI" ,DefineDec (VarPat "Root") (AppExp (VarExp "par") (ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "IsFleeing") ,ADTExp "Cons" [ AppExp (LambdaExp (VarPat "a")(AppExp (VarExp "check") (AppExp (VarExp "IsNull") (VarExp "a")))) (VarExp "GetNearestTarget") ,ConstExp NilVal]]) ,ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "IsAttacking") ,ADTExp "Cons" [ AppExp (LambdaExp (VarPat "b") (AppExp (VarExp "HpRateLessThan") (VarExp "b"))) (VarExp "GetFleeBloodRate") ,ConstExp NilVal]]) ,ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "IsNormal") ,ADTExp "Cons" [ AppExp (VarExp "loop") (AppExp (VarExp "par") (ADTExp "Cons" [ AppExp (LambdaExp (VarPat "c") (AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (AppExp (VarExp"IsNull") (VarExp "c")) ,ADTExp "Cons" [ AppExp (VarExp "LockTarget") (VarExp "c") ,ConstExp NilVal]]))) (VarExp "GetNearestTarget") ,ADTExp "Cons" [ AppExp (VarExp"seq") (ADTExp "Cons" [ AppExp (VarExp "seq") (ADTExp "Cons" [ AppExp (VarExp "check") (VarExp "ReachCurrentPatrolPoint") ,ADTExp "Cons" [ VarExp "MoveToNextPatrolPoiont" ,ConstExp NilVal]]) ,ADTExp "Cons" [ VarExp "Idle" ,ConstExp NilVal]]) ,ConstExp NilVal]])) ,ConstExp NilVal]]) ,ConstExp NilVal]]]))]
前面兩部分是我把在其他模塊定義的declares,選擇性地拿過來兩條。第三部分是這個人形怪AI的整個的AST。其中嵌套的Cons展開之後就是語言內置的List。
正如我們之前所說,做代碼生成之前需要進行一步類型檢查的工作。類型檢查工具其輸入是AST其輸出是一個檢查結果,同時還可以提供AST中的一些輔助信息,包括各標識符的類型信息等等。 類型檢查其實主要的邏輯在於處理Appliacative Type,這中間還有個類型推導的邏輯。形如(\a (Func a)) 10,AST中並不記錄a的type,我們的DSL也不需要支持concept、typeclass等有關type、subtype的復雜機制,推導的時候只需要著重處理AppExp,把右邊表達式的類型求出,合並一下env傳給左邊表達式遞歸檢查即可。 這部分的代碼:exp_type :: Exp -> TypeEnv -> Maybe Type exp_type (AppExp lexp aexp) env = (exp_type aexp env) >>= (\at -> case lexp of LambdaExp (VarPat var) exp -> (merge_type_env (Just env) (make_type_env var (Just at))) >>= (\env1 -> exp_type lexp env1) _ -> (exp_type lexp env) >>= (\ltype -> check_type ltype at)) where check_type (AppType (t1:(t2:[]))) at = if t1 == at then (Just t2) else Nothing check_type (AppType (t:ts)) at = if t == at then (Just (AppType ts)) else Nothing此外,還需要有一個通用的CodeGenerator模塊,其輸入也是AST,其輸出是另一些AST中的輔助信息,主要是注記下各標識符的import源以及具體的define內容,用來方便各目標語言CodeGenerator直接復用邏輯。 目標語言的CodeGenerator目前只做了C#的。 目標代碼生成的邏輯就比較簡單了,畢竟該有的信息前面的各模塊都提供了,這裡根據之前一個版本的runtime,代碼生成的大致樣子:
public static IO<Result> Root = Prelude.par(Help.MakeList( Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsFleeing) ,(((Box<Object> a) => Help.With(a, BaseAI.GetNearestTarget, Prelude.check(BaseAI.IsNull())))(new Box<Object>())))) ,Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsAttacking) ,(((Box<Float> b) => Help.With(b, BaseAI.GetFleeBloodRate, BaseAI.HpRateLessThan()))(new Box<Float>())))) ,Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsNormal) ,Prelude.loop(Prelude.par(Help.MakeList( (((Box<Object> c) => Help.With(c, BaseAI.GetNearestTarget, Prelude.seq(Help.MakeList( Prelude.check(BaseAI.IsNull()) ,BaseAI.LockTarget()))))(new Box<Object>())) ,Prelude.seq(Help.MakeList( Prelude.seq(Help.MakeList( Prelude.check(BaseAI.ReachCurrentPatrolPoint) ,BaseAI.MoveToNextPatrolPoiont)) ,BaseAI.Idle)))))))))總的來說,大致分為這幾個模塊:Parser、TypeChecker、CodeGenerator、目標語言的CodeGenerator。再加上目標語言的runtime,基本上就可以組成這個DSL的全部了。 上面列出來的代碼風格比較混搭,畢竟是前後差的時間比較久了。。parser部分大概是7月左右完成的,那時候喜歡applicative的風格,大量用了<$> <*>;後面的TypeChecker和CodeGenerator都是最近寫的,寫monad expression的時候,Maybe Monad我比較傾向於寫原生的>>=調用,IO Monad如果這樣寫就煩了,所以比較多的用了do-notaion。優化什麼的由於時間原因還沒看RWH的後面幾章,而且DSL的compiler對性能需求的優先級其實很低了,所以暫時沒有考慮過,各位看官將就一下。
((Box<float> a) => (Help.With(a, UnitAI.GetFleeBloodRate, Math.Plus(a, 0.1)))(new Box<float>())這裡Math.Plus屬於這門DSL標准庫的一部分,實現上我們就對底層數學函數做一層簡單的wrapper。但是這樣由於C#語言是pass-by-value,我們在構造這顆With的時候,Math.Plus(a, 0.1)已經求值。但是這個時候Box的值還沒有被填充,求出來肯定是有問題的。 所以我們需要對這樣一種計算再進行一次抽象。希望可以得到的效果是,對於Math.Plus(0.1, 0.2),可以在構造樹的時候直接求值;對於Math.Plus(0.1, a),可以得到某種計算,在我們需要的時候再求值。 先明確下函數調用有哪幾種情況:
public static Thunk<float> Plus(Thunk<float> a, Thunk<float> b) { return Help.MakePureThunk(a.GetUserValue() + b.GetUserValue()); }
如果a和b都是literal value,那就沒問題,但是如果有一個是被box包裹的,那就很顯然是有問題的。
所以需要對Thunk這個概念做一下擴展,使之能區別出動態的值與靜態的值。一般情況下的值,都是pure的;box包裹的值,是impure的。同時,這個pure的性質具有值傳遞性,如果這個值屬於另一個值的一部分,那麼這個整體的pure性質與值的局部的pure性質是一致的。這裡特指的值,包括List與IO。
整體的概念我們應該拿haskell中的impure monad做類比,比如haskell中的IO。haskell中的IO依賴於OS的輸入,所以任何返回IO monad的函數都具有傳染性,引用到的函數一定還會被包裹在IO monad之中。
所以,對於With這種情況的傳遞,應該具有這樣的特征:
所以With結點構造的時候,計算pure應該特殊處理一下。但是這個特殊處理的代碼污染性比較大,我在本文就不列出了,只是這樣提一下。
有了pure與impure的標記,我們在對函數調用的時候,就需要額外走一層。
本來一個普通的函數調用,比如UnitAI.Func(p0, p1, p2)與Math.Plus(p0, p1)。前者返回一種computing是毫無疑問的,後者就需要根據參數的類型來決定是返回一種計算還是直接的值。
為了避免在這個Plus裡面改來改去,我們把Closure這個概念給抽象出來。同時,為了簡化討論,我們只列舉T0 -> TR這一種情況,對應的標准庫函數取Abs。
public class Closure<T0, TR> : Thunk<Closure<T0, TR>> { class UserFuncApply : Thunk<TR> { private Closure<T0, TR> func; private Thunk<T0> p0; public UserFuncApply(Closure<T0, TR> func, Thunk<T0> p0) { this.func = func; this.p0 = p0; this.pure = false; } public override TR GetUserValue() { return func.funcThunk(p0).GetUserValue(); } } private bool isUserFunc = false; private FuncThunk<T0, TR> funcThunk; private Func<T0, TR> userFunc; public Closure(FuncThunk<T0, TR> funcThunk) { this.funcThunk = funcThunk; } public Closure(Func<T0, TR> func) { this.userFunc = func; this.funcThunk = p0 => Help.MakePureThunk(userFunc(p0.GetUserValue())); this.isUserFunc = true; } public override Closure<T0, TR> GetUserValue() { return this; } public Thunk<TR> Apply(Thunk<T0> p0) { if (!isUserFunc || Help.AllPure(p0)) { return funcThunk(p0); } return new UserFuncApply(this, p0); } }
其中,UserFuncApply就是之前所說的一層計算的概念。UserFunc表示的是等效於可以編譯期計算的一種標准庫函數。
這樣定義:
public static class Math { public static readonly Thunk<Closure<float, float>> Abs = Help.MakeUserFuncThunk<float,float>(System.Math.Abs); }
Message類型的Closure構造,都走FuncThunk構造函數;普通函數類型的構造,走Func構造函數,並且包裝一層。
Help.Apply是為了方便做代碼生成,描述一種declarative的Application。其實就是直接調用Closure的Apply。
考慮以下幾種case:
public void Test() { var box1 = new Box<float>(); // Math.Abs(box1) -> UserFuncApply // 在GetUserValue的時候才會求值 var ret1 = Help.Apply(Math.Abs, box1); // Math.Abs(0.2f) -> Thunk<float> // 直接構造出來了一個Thunk<float>(0.2f) var ret2 = Help.Apply(Math.Abs, Help.MakePureThunk(0.2f)); // UnitAISets<IUnit>.HpRateLessThan(box1) -> Message var ret3 = Help.Apply(UnitAISets<IUnit>.HpRateLessThan, box1); // UnitAISets<IUnit>.HpRateLessThan(0.2f) -> Message var ret4 = Help.Apply(UnitAISets<IUnit>.HpRateLessThan, Help.MakePureThunk(0.2f)); }
與之前的runtime版本唯一表現上有區別的地方在於,對於純pure參數的userFunc,在Apply完之後會直接計算出來值,並重新包裝成一個Thunk;而對於參數中有impure的情況,返回一個UserFuncApply,在GetUserValue的時候才會求值。