Since Ruby RSpec let and let! diffirences, I've been working to understand the implications of converting an existing codebase, one which is inherently imperative to one that implements and utilizes more functional paradigms. The existing codebase is a prototypical game I wrote for my final project/coursework in my Computer Games Architectures class.
The game is written in C# and uses MonoGame as the primary game engine.
The task at hand, is to implement distinctive elements of functional programming without changing the meaning of the game, its gameplay and rules, in effect refactoring it.
From an implementation point of view, its important then to define what are those elements, techniques, paradigms or indeed styles that are inherent to functional programming and to which I would need to implement to be faithful to those ideals.
Functional Programming paradigms
My views are that paradigms are foundational elements, cross-cutting concerns and in this way the primary paradigms are reliability, using higher-ordered functions and ensuring immutability during computation.
One might argue that reliability and higher-order functions are unique, while immutability results in reliability (as does inherently pure functions).
Reliability and Pure functions
This is an absolute guarantee that a function will always yield the same results when the same arguments are used on subsequent invocations. Aspects of programming that undermine this guarantee as constructs or practises that may yield exceptions to this idea.
This includes the introduction of varying probability such as this potential in Input/Output operations & processing, exceptions, and mutability of state. Anything else that may contribute to the varying probability of a function is also of concern, including language features such as NULL which, when passed in as arguments to a function can cause functions to stray from their guarantee of reliability and robustness.
This is all particular to computation within a function, with the ideal function being one that is so-called 'pure'.
Ensuring reliability
- Preventing functions throwing any exceptions
- Preventing arguments being invalid such as NULL (see smart constructors that validate on construction)
- Writing pure functions
- Isolating I/O to 'the fringes'
Using Higher-Ordered Functions
A distinctive trait of functional programming is the casual manner in which functions are passed into other functions. Functional programming treats functions as first-class citizens, in so much as they can be passed as functions.
In Lambda calculus, this notion is represented by the concept of abstractions and applications. (Dudak 1989)
Immutability of program state
A central concept in functional programming is reliability. Modifying state after the fact, during or before processing can impair a function's guarantee to be reliable.
In much the same way that exceptions and I/O may produce different results irrespective of whether the same function input arguments if those arguments themselves are changed during the processing by the function, it's not clear or guaranteed that the impact of the function has not invalidated state.
Arguments that are used or depended on outside the function should not be modified during the function execution.
The practical way to achieve this is to make any modifications on any data non-referencable by any other code, other than the function in which it was modified, that is to to make an exclusive copy of any data arguments that are passed in and modify that. In so doing it makes it impossible for other code that originally had access to the data that was passed in as arguments, to modify that data after the function exits(and thus invalidate any guarantee the function makes).
This relies on copying data, releasing it, and ensuring it cannot be modified after it leaves the function. Other options are listed below.
Ensuring immutability of data
- Copying argument data if it is not immutable.
- Immutable data types could negate the need to copy as they could not change after the function executes, without creating a new copy.
- Creating your own immutable types - such as classes with smart constructors and get only accessors etc.
- Using existing C# immutable data types
- Assignment statements are scrutinised (no assignment to global state incl. instance members and other non-local state)
- Member functions that act on a class's data either do not do this or are converted to static functions that thus explicitly cannot.
Declarative style or organisation of computation
Functional Programming techniques
In my view, techniques are ways, aspects or methods that bring about the foundational paradigms: reliability and higher-ordered functions.
Any means in which to achieve this, I consider to be techniques, such as ensuring immutability. These are based on decisions the programmer makes to achieve these.
This might include using language dependant attributes like immutable data types, or external libraries such as LanguageExt. This might include removing NULLs by utilizing Option<T> or removing the throwing of exceptions through Try<T> etc.
Techniques
- Using immutable data types
- Immutable Dictionaries, Tuples
- Copying data
- Not throwing exceptions or implementing a means to suppress exceptions (Try<> and Ensure) into a standardised form such as a failure return type.
- Encapsulating failures in return types such as using Either<Failure,R> to maintain the reliability of function always returning uniformly despite exceptions, I/O problems etc.
- Converting functions into pure functions
- Moving functions to be static
- Removing access to instance variables
- Removal of operating on non-local data, mutable types (such as copying it first to make it local
- Pipelining(including Binding() and Mapping()) to declaratively construct logic blocks using Monads and Functors
- Using smart constructors
- Implementing and designing to deal with I/O in an isolated way or pushing to the fringes design
Distinct Benefits of Functional Programming In General
Functional programming is only as useful as its benefits. Reliability being the most crucial in my opinion. Others are listed below.
- Reliability (Immutability, function guarantee etc)
- Parallelism (Immutability)
- More expressive formation of logic (declarative)
- Conciseness codebase (declarative)
- "shorter programs tend to have fewer bugs"15
- Lazy evaluation through HOFs (only execute if the condition is met)
Distinct Specific techniques of Functional Programming In General
Also, specific techniques are useful in trying to obtain the ideals of reliability too:
- Lazy evaluation of transforming functions and Short-circuiting (Monads in general)
- Lazy evaluation through HOFs (only execute if the condition is met - see Binding and Mapping for data extraction)
- No exception handlers (Try<T>, Either<L,R> and Ensure(), EnsuringBind() etc)
- Removal of NULL (Option<T>)
- Using Bind() on and Either<> removes requiring to write repetitious/builder-plate If..else code for Left or Right circumstances
- Reasoning about contents of variables is easier (Option<T> has no NULL), Either<L,R> will only allow for two outcomes in the type
- The non-happy path is always accounted for with Option<T> or Either<L,R> when extracting values using Match()
- Memoization and caching
In many ways, I think functional programming as it's used in programming these days, especially in industry (read: business) is a mix of what is purely functional ideas such as those introduced through lambda calculus, including strict immutability and those that are inherently not but which are required in order to be particularly productive in the mainstream of imperative programming: sequential state change, exception handling, NULLs, mutable datatypes.
I'm trying to rewrite a game prototype I wrote for my Game Architecture course to implement these ideal.