Why, I do declare!
Guest post from Anonymous Rust Dev
Back again for more, The Anonymous Rust Dev is here to talk Declarative Code.
You’ve seen it before. Don’t lie, I know you have. Java has the Stream API. .NET has LINQ. We’ve even talked about lazy evaluation ourselves.
When we last looked at lazy evaluation, it was in the context of doing work only when it’s needed, rather than preemptively doing all the work and risking that much of it wasn’t needed. YAGNI.
All that said, these streaming models expose other benefits. And I, your friendly neighborhood Anonymous Rust Dev, am here to tease some of them out.
Declarative code
First, a shameless plug for our recent conversation around declarative pipelines. In it, Dan looks at Spark and the SDP framework with examples.
SDP isn’t the first time declarative code has appeared on the scene. In fact, traces of it can be found back in the coding antiquity — ML originated in the early 1970s and is the precursor to many modern functional languages like Haskell or Scala. And I know my audience; SQL’s
SELECTIt is another popular declarative grammar.
It’s understandable how imperative became the lingua franca of the programming world. For instance, just looking at C, one of its goals is to closely mirror what the underlying hardware is doing, and if you’ve ever cracked open assembly code, you’ll see imperative code at its most fundamental level. For those who don’t know assembly, I don’t plan to show you any code today, but it’s literally just a sequence of atomic steps like “add this to that” or “go to this location in code”.
Procedural code is easier to understand when your scope is small. It’s very easy to reason about an addition statement. But when looking at large-scale problems, many tiny instructions start to turn into noise. Naturally, you can (and should) refactor your code to hide some of that. In a perfect world, if you’ve done a good job of refactoring and structuring your codebase, the code should flow like prose — or, to put it another way, it should tell a clear story.
And that is where declarative programming comes in. Baked into its entire premise is the notion that, rather than giving you a series of do-this-then-do-that statements, it instead models the flow of an application, and more closely represents many problem domains.
Functional programming
The “functional” in the name isn’t talking about whether or not it works, but rather the unit of work being employed. If this video by Brian Will doesn’t quickly sour you on OOP, I’m not sure how long you’ll be able to stick with me; the core premise I take from it is that OOP doesn’t really model how things work in the real world. Conversely, if you’ve ever seen a flowchart and understood it, you intuitively know functional programming.
Don’t get me wrong, OOP has its place, but it’s overused and employed for the wrong problems. If you’ve ever spent time in old-school Java and C#, you understand this better than most — everything is derived from Object, and if you want to execute behavior in your code, the recipe is:
Have a thing
Have the thing do something to itself via methods
Sounds passive to me. All of your code starts on the premise that it’s attached to a thing, and at some point, the “you must have a root thing to do all the stuff” narrative became so tiring and tedious that C# introduced syntactic sugar to alleviate it with their “top-level statements” concept. Now, you don’t need a bunch of namespace and class boilerplate to start doing stuff — it’s still there, lurking behind the scenes, but programs start to feel more like a series of actions and less like a bunch of objects that are being forced to stand in for behavior.
When functions are first-class citizens, behaviors are easier to express... and arguably, to understand. Where OOP inevitably drives you to use class structures and inheritance models to contain, protect, and manage application state, functional code tends to focus on “having data” and a means to transform or process it.
Now, that recipe becomes:
Do stuff
In practice
Relax, I’m not trying to convince everyone here to become a Scala dev. While not as elegant, Python has some constructs that make this somewhat ergonomic.
First, I’ll introduce you to the notion of a “point-free” or tacit style of programming. That Wikipedia link offers a Python example that illustrates how to “compose” a series of functions into a wrapping function.
This should intuitively make some sense when you see it. If you’ve ever baked a cake, you understand the black-box idea of “baking a cake,” even without memorizing the individual steps. Of course, you realize there are in fact several atomic steps that must be executed (e.g., procure eggs, store/refrigerate eggs, remove eggs from storage, break eggs, blend eggs, etc.), but many of those details are composed of parent steps that themselves are composed into the recipe as a whole.
Functional programming follows the same spirit. I’m switching to TypeScript for this illustration, since it does a good job of being readable while still giving us type descriptions:
interface Ingredient {
name: string,
quantity: number,
units: 'each' | 'grams' | 'milliliters',
}
type Cake = unknown; // We'll figure this out some other time
function bakeCake(ingredients: Array<Ingredient>): Cake {
// TODO
}First, you can see I “baked” in some assumptions (sorry, couldn’t help myself) — namely, that you need ingredients as an input to produce the output Cake.
Want to see something cool? The function signature for a “good” (subjective measure) function should tell you something even without knowing the function or argument names: (Ingredient[]) -> Cake. You intuitively know, even without being told what the function’s name is, what recipe is being executed:
Scenario: (unnamed behavior happening here)
Given some ingredients
When we (insert function name here)
Then we produce a CakeBDD (e.g., Cucumber’s Gherkin, as shown here) is an amazing way to test-drive systems, particularly those with an emergent design like the one we’re using here. We “know” there are some steps to producing a cake, and we can black-box that behavior in the meantime with a Scenario until we’ve had a chance to do some discovery and tease it out.
Returning to our bakeCake function, let’s try stubbing out some behavior:
function prepare(ingredients: Array<Ingredient>): Array<Ingredient> {
let processedIngredients = [];
// TODO: take input ingredients, do stuff do them, produce some "prepared" ingredients
return processedIngredients;
}
type Oven = unknown; // Also figure this out at a later date
function preheatOven(tempCelsius: number): Oven {
let oven;
// TODO: do some stuff to make our oven hot here
return oven;
}
interface Food { /* TBD */ }
// Fleshing out Cake just a bit more:
interface Cake extends Food { /* TBD */ }
function cook(ingredients: Array<Ingredient>, oven: Oven): Food {
let result;
// ???
return result;
}
function bakeCake(ingredients: Array<Ingredient>): Cake {
const preparedIngredients = prepare(ingredients);
const preheatedOven = preheatOven(175.0);
const cookedFood = cook(preparedIngredients, preheatedOven);
return cookedFood as Cake;
}Actually, if we’re being honest, the process of baking a cake doesn’t invent a new oven in the process; we’ve teased out a hidden requirement that makes me want to shuffle some stuff around to be more honest:
function preheat(tempCelsius: number, oven: Oven): Oven {
// do something to our input oven to make it the right amount of hot
return oven;
}
function bakeCake(ingredients: Array<Ingredient>, oven: Oven): Cake {
const preparedIngredients = prepare(ingredients);
const preheatedOven = preheat(175.0, oven);
const cookedFood = cook(preparedIngredients, preheatedOven);
return cookedFood as Cake;
}While still sketchy on the details, we can now see the emerging design of a cake-baking workflow. Also, I have enough in place to refactor with composition in mind (relying on type inference where possible):
const bakeCake = (ingredients, oven) => cook(
prepare(ingredients),
preheat(175.0, oven)
) as Cake;You’re free to have whatever opinions you like, insofar as to whether you prefer the refactored version or the more imperatively-styled variation that preceded it.
However, you have to admit, the relationship between input and output is clear enough at either point to describe it in high-level terms (e.g., via a flowchart):
Yeah, this is the first time we’re actually seeing the ingredients enumerated, but I couldn’t leave you hanging forever...





