Other Aspects of F# · These are questions for wise men with skinny arms

Other Aspects of F#

Learning F# as part of a real project is more than just the application code, it’s everything that’s involved with building up the system requirements to deploying updates. At this point in my adventure my attempts at working FRP into the solution weren’t proving fruitful, so I found it easier to focus on the rest of the ecosystem and other aspects of the solution.

Testing

There was an upside to creating a system that didn’t work; to discover why it didn’t work I needed a much more powerful testing framework than before. The number of possible connections between components had increased due to the connectivity of events and I wanted a technique that could handle this scale of complexity. Manually discovering and testing probable inputs would have been very time consuming since each component could operate independently of the other event stages, so the contracts among them were very loose.

I was always excited by the idea of generative testing, but haven’t ever taken the opportunity to try it in practice. I pulled in a port of QuickCheck for F# and wrote a few trivial tests. They failed in very strange ways, where I had to do more work adjusting the tests to not do stupid things than I spent actually finding bugs in my code. Even with that setback, I still liked the idea of property based testing. It covered some great use cases where I really wanted brute force testing of external interfaces. I also found that when porting my tests into F# that they were previously testing trivial things.

Most of the tests I had in C++ were for edge cases that were specific to the implementation rather than to the business logic. The tests were more concerned with crashing or violating system invariants than proving example use cases. With fewer system invariants in play because of the reduced scope of state, F# was constrained in ways that were useful when writing business case tests. A good chunk of the old tests were completely obsoleted by the use the options and custom discriminated unions. The contracts I couldn’t enforce through types were often complex and transient. These required more meticulous configurations with what would have been the equivalent of integration tests in the previous solution.

I tried to write my F# such that I exposed all error conditions as part of the control flow instead of relying on exceptions even for simple things like out of bounds errors. This made it easier to test specific conditions that I wouldn’t normally expect, but weren’t impossible. The additional constraints and robustness across the code actually made trivial tests more difficult to write. The tests I did write could focus on the real properties of the application as it pertained to a single piece of functionality. There wasn’t as much ‘setup the world’ requirements since the small components couldn’t see anything outside of their inputs. These tests ended up being much more exhaustive than before, and uncovered a few corner cases where the desired behavior wasn’t well specified, but was just determined by accident by the implementation. I found a few situations in my old solution where I was surprised because I hadn’t considered the total implication of my implementation, but still managed to avoid real show stopping bugs. Even the minimal level of testing I did in F# made the previous ‘battle-hardened’ version look fragile in comparison. I’m sure I could have used a property based test framework in C++, but I don’t think it would have been as friendly to use without all of my domain types constraining the inputs.

Clarity

I hadn’t substantially advanced my knowledge of new F# features or syntax since I started the FRP work, but I did find myself cleaning up very strange pieces of code that I had written less than a month earlier. I could see where attempting to be clever with type deduction or first class functions started to bite me. I was dangerous enough to use things like partial application when I saw an opportunity, but I wasn’t used to reading or expecting code that relied on it. I was learning the hard way of what features not to use in certain situations. The biggest problems I had when re-reading code was when multiple partial applications were chained and some functions were inserted as arguments that would change the flow or return value implicitly. My previous reliance on types with member methods stymied type inference, and the VS tools didn’t help much when working with functions that had required a type hint before. The problem wasn’t fighting type inference as much as trying to figure out why I had jumped through all these hoops in the first place.

When doing some of re-work I found that the type system didn’t help with correctness as much as it did with documentation and intent. A specific type gives context on usage where a name alone would be insufficient for a value, especially for data structures. Most of the data structures I used were slippery on correct usage, as I often didn’t go the extra mile to forbid certain usage patterns based on my business rules. My experience before with this is that I generally go overboard and provide and constrain the interface that isn’t flexible in ways I need later and still forgets to verify some contextually implicit invariants. Providing the perfect model is more difficult than just making usable functions for the rules at hand, which is what most OO wrappers boil down to anyways. But with this lax approach to typing, I could match signatures pretty well and still completely screw up the logic! I’d have to take a step back to see the usage of the types in context to I find where I had gone wrong. I had to standardize my approach to working with values in collections so that I’d have consistent assumptions for each (Only grow arrays from the tail, only grow lists from the head). This seems obvious in retrospect, but I hadn’t firmly established conventions like this before and I got bit a few times by trying to be clever with assumptions. I briefly looked at things like SortedList to manage assumptions about values, but I didn’t want to rely too heavily on systems like that where I’d be better off with good tests instead of robust components that would hide failures of understanding or just plain programming errors. I found that even straying a little bit into C# collections caused some pain in F# because the semantics of the language were so different. I’d have to cast off units of measure or unzip pairs in order to interop with C# code or other complex external libraries. To keep the code clear I also tried to keep it using common constructs instead of making every piece special purpose.

Working through each interface as I was breaking it out into FRP let me see what each piece of code should have been doing. Once I cleaned up these complicated pieces of code, I felt that the majority of the code was simple enough to read, even it was terse at some points. I found that once I was more familiar with the easy patterns of abstraction, semantically compressing uninteresting code became more obvious, faster to perform, and less error prone. Where in C++ I’d sometimes struggle with the right level of abstraction and complexity for reducing repeated code, F# functions and collections were great for abstractions that were conceptually and syntactically simple.

Patterns

As part of the refactoring process to put everything into streams, I changed many functions to take inputs as structs instead of many individual values, since those structs would be contents of the stream. I found that code with fewer, more specific inputs was easier to develop and comprehend, but harder to re-use and test. Requiring more than what was necessary (assuming not every field would be used) meant that for testing or other situations where that struct wasn’t already available it would require an additional step to construct it. Taking this a step further I found that functions with fewer custom intermediate records composed most easily. Having custom types everywhere without structural subtyping meant doing explicit conversions at many stages. When doing work deep within a pipeline it easier to work with rearranging values than converting records.

Sometimes my abstractions were too narrow and took too many inputs, and other times they were oversimplified and took only one. The ability to write small inline functions was critical to bridging the gap between different function styles. I switched the format of many of my abstractions to be consistent at each layer, so that I wasn’t jumping between many different data types within the same function nor was I completely hiding the complexity of the contained logic. The tradeoff was that I still had unwrapping logic at each layer, where it would perform an operation and unwrap the parameters for later use. Finding an internally consistent level of abstraction within each function was a huge mental gain for me, on the level of the SRP for OO.

To further reduce some wrap-unwrap logic, I found an excellent trick to reduce the number and complexity of arguments. If the function didn’t directly work with a value and only passed it along to an internal function, then it didn’t need to be a separate argument. Passing through the same value through multiple levels of functions, like one would with wrappers of objects, could be eliminated. If a dependant function needs the value that has already been unwrapped, the dependant function can be passed in as an argument with the parameter partially applied where it was first unwrapped. This will make the function more complex to understand, so I didn’t standardize on it, but I found it useful where I would have had a dozen parameters to apply at once because wrapping one layer at a time per function wasn’t fast enough. The language didn’t guide me in this regard, and I can see it’s a bit heavy handed to support the function interface consistency. I found the many choices for abstractions made picking the best one for any situation more difficult, but because each abstraction was well contained and strongly typed it wasn’t hard to fix later if I got it wrong.

I was starting to see and focus on the nuances of composition. It wasn’t just how to get it done but exposing the many facets of why a certain technique was the best. Simplifying my data model to reduce interdependencies and increase language consistency was a big gain, but this was still in the context of being hamstrung by how to organize the system at large with FRP.

Tools

It was around the time that I was trying FRP that I also tried out VSTS and TFS. The gains from having a well managed environment were becoming much more clear. Python’s packaging and distribution wasn’t perfect, but it was miles ahead of the non-solutions that C++ had out of the box. Just setting up the C++ build environment was a very manual process because of the number of external and system dependencies. For a one-man project, it was a ton of overhead with no real gain. The fact that I could push my code to a remote server and have it build just like on my machine with 0 minutes of manual setup was magical. This says more about how much effort CI providers put into their C++ toolchains than working with C++ on Windows. The ability to get a no-hassle managed build helped encourage me to, at the very least, migrate away from C++ for this project. I had serious problems with TFS where support’s best answer was “delete it and start over”, so that didn’t make the cut.

The other missing tools like code analysis and test coverage started to make less of a difference with increased warning levels on the compiler. It gave hints in some places, but I didn’t need to avoid as many footguns as before, so I wasn’t looking for as detailed guidance. The debugging experience was still weak because of how difficult it was to inspect the return value of pipeline functions. I sometimes found myself breaking it up just so I could easily pin the values as I stepped through. The REPL also wasn’t all that useful since what I really wanted was a Javascript like debugging experience, where I can write normal code to modify the state of the program and then just continue. The REPL was great for testing assumptions without pouring over the docs or writing a standalone sanity test, but I couldn’t really find a good use case for writing scripts to do something useful yet.

All in all the tools weren’t awful, but it looks like the F# ecosystem is heading towards web services and away from analytical ‘desktop’ progrogramming that would benefit this kind of project.

Comparisons

I put the FRP work on the shelf for 4 months and went back to working in C++ and Python. I brought a few new ideas back with me. STL algorithms made much more sense than before with all of the functions as arguments. I could practically use it without the documentation, where as before it had been a niche tool that I only used based on SO mentions. In Python I found myself bringing in functools for decorators and began to abuse list comprehensions in even more creative ways than before. I missed a few features that I didn’t even realize weren’t language specific, things that started as annoyances in F#, but made much more sense after I switched back. The uniformity of most syntax being an expression instead of a statement is a huge gain so you can do the most basic:

Special_initial_condition = if special_condition then 129 else 0

Without special syntax, a forward declaration, or the possibility to accidently not define that symbol at all. Many of the functional constructs that I originally chalked up to ‘funky syntax’, were much more powerful than I had given them credit. Simple things like the order of arguments in calls, the function composition operator, and the syntactic equivalence of a value and a function. I had already loved things like easy tuples and powerful data structures from Python that were generally painful or awkward in C++. This feature envy caused my C++ to look strange from time to time, but learning the limits of the language was enlightening.

Versus Python

My F# constructs that I brought back to Python made my code look awkward when emulating the pure functional data flow. I could do the same data structure acrobatics in Python, but it was more error prone. I didn’t have the same IDE support and compiled type sanity to see if the types I was building would provide the interface I needed. Knowing the interface is at least minimally correct while typing is much faster than writing or even running existing unit tests. F#’s strict explicitness made it more difficult up front to design, but easier in the long term to extend and understand. When I’d attempt to abstract some repetitive structure in Python I’d frequently go back to ‘un-functionalize’ logic that used anything more than simple collections of values or tuples.

While Python could do everything F# did and more, doing complex work in Python required a managing a much larger mental burden. It was much easier and more Pythonic to not simplify complex setup or connective logic, but this sometimes lead to later bugs. Passing functions as arguments or making deeply nested data structures were prone to fail in too many places when building larger Python systems. Python’s functional tools for data structure manipulation work at a lower level than what F# collections provide, causing more boilerplate when composing and performing non-trivial transforms.

With F# I’d write a function once and the compiler would preserve it from most unexpected changes, even if it was a bit more complex or verbose than it would be in Python. In Python, the code would start simple and appear to stay correct but slowly ‘rot’ as the logic mutated around it before breaking from an external change. Even a layer of unit tests with good coverage doesn’t prevent edge cases that weren’t considered when the tests were first implemented. Every function in Python is just so flexible and had such a large testable surface area that it sometimes feels like plugging holes in a sinking ship once a function higher in the call chain starts misbehaving. It takes a serious dedication to testing to write dependable Python code in the face of breaking previously held assumptions about the state of the system.

I preferred having the type system enforce assumptions about the code so I could keep everything consistent and see the real impact of my changes in the code. The type inference was good enough so retaining the interface doesn’t require tedious updates on every change. Python’s duck typing was better in places where I could disguise a custom data structure as something else to match an interface, but as I pushed the boundaries or extended the use cases, the code tended to fray from the intended interface in dangerous or unexpected ways. The shortcuts that are so easy to take in Python to make incredible progress eventually come back to make life more difficult if large changes need to be made.

I did still love Python’s flexibility, completeness, and functional tools after working in a more constrained environment. F# had the strange relationship of not providing as many powerful general purpose tools and instead focusing on single purpose ones that could be more easily composed. When I talk about the Legos vs Duplos analogy between C++ and F#, Python would be more like PlayDoh. Even easier than Duplos to get shapes right, but possibly messy. Python’s surface simplicity and fantastic libraries made it easier to just ‘bang-out’ out a minimal solution while completely hiding some real edge cases. It had all the tools to get most problems to 80% solved quickly, but it really struggled getting that last 20% of features and polish that didn’t make it in the first pass. There are more than few projects where that last 20% of robustness or complex features aren’t even needed, so using Python has no practical downside. F# takes more thinking up front and didn’t bring the same weight and usability of the Python ecosystem (at least for scientific software and simple REST APIs), but it was a much smoother transition at the later stages of complexity into a larger, more complex system. These are interesting tradeoffs that again gave me serious pause on the use of F# over Python as the primary language in the system. If F# hadn’t turned out to be as useful as it did, I probably would have transitioned from C++ to Python. And in comparing the three languages F# is clearly closer to Python than it is to C++.

Gains

Using F# for an extended period of time generally opened my eyes to implicit choices I was making in structuring my applications. Forcing everything to be an object pushed the data model and code organization to be excessively layered and generic. Once I started working more exclusively with functions, I saw where objects would still be useful to simplify and organize concepts bigger than a single function. It would have taken me much more time or much more pain with my previous set of languages to notice the patterns that I picked up quickly in F#. This work gave me enough perspective on some non-OO ways of thinking that I could more honestly evaluate the appropriateness of objects.

But for the code itself, I didn’t want to keep much of anything from the experiment with FRP. It only took a few days to get stuck in the mud, but I learned so much by going off the beaten path and exploring other aspects of the language environment. Working at a deeper level of complexity showed an entirely different side of the language than if I had continued to just work on prototype or toy level systems. There was a very big jump in understanding once the foundation of the systems were in place, without which I don’t think I would have seen any of F#’s real benefits. Languages that only work ‘at-scale’ are bothersome because you have to be a little bit crazy to even get there.