| MoMonkey 的个人资料Monkey Coder日志列表网络 | 帮助 |
|
|
5月9日 Popfly Game CreatorAnd I thought Scratch was cool. Dang, Popfly Game Creator rocks! Be sure to watch the video... 5月2日 Closures to the RescueA coworker just walked into my office with an interesting refactoring question; hoping an FP-style would help. I think it’s a decent example of how mutation generally gets in the way of compositionality, but also of how FP can still be applied in a mixed world like this; a good enough example, I thought I’d blog about it. Basically he has a standard “hole in the middle” problem for which FP is normally perfect, but in his case the code is written in an imperative style and so the “holes” have to share state. Essentially, he has some retry logic that he wants to reuse. The “holes” are to A) initialize some state, B) make multiple attempts to do something that may fail (e.g. network requests, etc.), and C) finish by cleaning up and generating a result of some kind. Example First we need something dangerous to try (and retry upon failure). For simplicity, I’ll just use a silly DiceRoll() function that blows up 1/7th of the time: private static int DiceRoll() Then, a super-simplified version of some kind of retry logic; in this case returning the sqrt of 2 + a random dice roll (real useful stuff). It’ll give it three chances before giving up: // A) init (mutation!) We’d like to reuse this logic; plugging in new implementations of the A, B and C “holes”. How do we do that? From an FP mindset, the first thing that comes to mind is to bundle it up as a function taking three higher-order functions (A, B and C) as arguments. The Problem In a pure functional world, that would be perfectly easy and you’d be free to compose any arbitrary (independent) implementations of A, B and C. The main problem here is that B depends on the state left behind by A (in this case, the value of ‘x’), and C depends on the state changes made by B; they’re not independent. Shared state makes it messy (this is why FPers avoid it). By definition this gets in the way of composing “mis-matched” A/B/C sets, but it shouldn’t get in the way of generally passing in higher-order functions to factor out the reusable retry logic. We just need some way to pass around separate functions that share state. OO Solution The OO programmers first thought is to not make them entirely separate functions, but to make them separate methods of one object containing the shared state. We’d start out with an interface or abstract class: public interface IAttemptDangerousThings Then we’d implement the “holes”: public class AttemptMe : IAttemptDangerousThings Finally, we’d make a function that bundles up the retry logic and takes instances of this interface type (or make it a member, but then it’s harder to mix and match with different ‘retry’ implementations). public static void OOSolution(IAttemptDangerousThings doSomething) We get the compositionality we wanted. We can even use inheritance to compose various versions of A, B and C (in limited ways). FP Solution The idea of refactoring the way we have so far is already an FP-type idea, but the main reason I dislike the OO “packaging” of higher-order functions with shared state is just that it’s verbose; having to construct whole class hierarchies just for “packaging” and also I think inheritance is a clunky way of allowing mixing and matching A’s, B’s, and C’s. I’d much rather just use a delegate: public delegate void DoSomething();
And change the retry logic to take three of these guys: public static void FPSolution( Then it’s very simple and lightweight to throw together the pieces: int y = 0; // what the?! delegates can access this? Closures! What might look strange about the three anonymous delegates above is that they all refer to ‘y’, which is defined outside of their scope! If you haven’t seen this before you might be wondering if that really compiles, and what happens when the locals go out of scope but the delegates still exist on the heap. If you’ve always thought of delegates as just type safe function pointers, they’re more than that. They are ‘closures’ that can ‘capture’ the environment in which they’re first created. They can even be returned out of their original scope and continue to capture out-of-scope variable (such as ‘y’). It’s pretty cool stuff! In fact, who even needs OO? Really, “objects” with private state and methods that share it can easily be simulated with closures. Plenty of OO systems with inheritance, polymorphism and the whole shebang have been built in Scheme this way. It’s an age-old thing in FP languages, but new in C# since anonymous delegates were introduced. Anyway, now we can just pass these delegates/closures into our FPSolution(init, attempt, finish); and voila! |
|
|