Low Code is Dead. Long Live Low Code!
An Introduction to the General Theory of Low Code Relativity
History Rhymes
Technology providers ride the 24-hour hype cycle as much as they drive it - and with that comes peaks and valleys in the signal-to-noise ratio online. This week was Microsoft's "Build" showcase - and with that comes their big marketing push to companies and the broader development community. I usually don't pay much attention but I wanted to check out how they pitch the road map for .NET and surrounding tools. It can be useful to see how they're positioning their wares to developers and the decision-makers that green-light the projects that use them. I was also curious to see how Microsoft would balance between playing to their enterprise customers while flexing both their purchased and earned-equity in open source.
"History doesn't repeat itself. But it often rhymes."
-- Mark Twain (apocryphal)
One of the reasons I roll my eyes at these types of events is the inevitable low code product roll-outs. This is a staple of the enterprise so I would normally let it pass without mention, but well - here we are. Over the past few years I've worked as a consultant, and a considerable portion of that time is spent helping companies recover from low code "buyer's remorse". And while many of my examples here point to Microsoft, they're not the only name in the low code game. Far from it. It just happens that the latest offerings read like deja vue all over again.
I remember when Microsoft was straight-faced telling their customers that weaving hand-crafted XML was a valid activity for non-developers. (Looking at you, InfoPath.) Now there's PowerApps which at least is moving the WYSIWYG interface to FrontPage levels of functionality. Remember when SharePoint Composite promised the ability to create a business solution "without programming"? Their marketing might prefer you forget. They've since moved on to Teams as the facade for what they're calling Project Oakdale and PowerBI continues to merge with Excel in the form of Power Apps and Power Fx.
Common Sense or Snake Oil?
I'll admit that my skepticism toward the concept of low code marketing is both anecdotal and fed by long held sentiment in the developer community that it's an inherent boondoggle. But aside from arms-length appraisal I've also had to rescue projects from the adverse consequences of poor low code implementations. Is it possible that there are so many well-functioning low/no-code systems I never see due to the "Maytag Repairman" effect. Those solutions simply quietly go about doing their work without notice. But given that Microsoft changes the names of these products while changing the "face" from Office for SharePoint, then SharePoint for PowerBI, then spreading the joy from PowerBI to Teams, I'll stick with my thesis for now.
Cautionary Tales
In one case I saw a company's "low code" web form that fed into their recruitment system, and the form requested information that was not legal to ask. In another situation a healthcare company had run amok with data underlying PowerBI reporting such that a "multi-million-dollar investment" had to be re-tooled from scratch. And at a financial firm I saw a group of traders use SharePoint workflow to play a hidden shell game with positions that caused the comptroller to throw a fit when they found out about it. While tech companies with enterprise offerings will sell the sunny side of "citizen developers" I've seen enough of the dark side to be reflexively skeptical. The executive presentation may look like virtuous incentives and maximizing productivity, but lurking right behind the shiny facade are perverse incentives that give short shrift to good engineering practice and is often the basis for the dreaded "shadow IT", with operational and legal risk that follows with it.
Low Code On The Down Low
But there are many, many examples of actual low code in service that checks all of the boxes, it's just not the shiny facade with slick presentations at trade shows. I was reminded earlier this year when reading an article from Steve Smith [aka Ardalis]. It echoed some ideas I had about java and .NET. But it really made a succinct if inadvertent case that "low code" is everywhere now.
If a group in your company builds an API for your team to use in an internal application, that's low code. If your company uses a third party service to broadcast messages to a mailing list, that's low code. If an application makes a request to the open weather API or other public endpoint, that's low code. Behind each of those examples and a myriad others there's work going on behind the scenes that the caller will never know about - and that's the beauty of it. But there's a catch. There's always a catch, and it's all about context.
You still have to "mind the store" around certain base concepts, and recognize the unavoidable cognitive burden involved in carrying enough of what's happening in that gray box of functionality so that you can maximize its advantages and limit its pitfalls. And never mind if that system or solution you depend on puts out a breaking change that nukes your project. No one selling you a "low code/no code" solution will talk about that, and for good reason. If you knew the total cost of that ownership, you'd never buy in, and that works against their interest. But for now I want to set that aside - because the more interesting bit to me is how software engineering today has become first-and-foremost a low code world. And it's so pervasive that almost no one talks about it in those terms.
DDD, DSL, ORM & Alphabet Soup
I didn't recognize it at the time, but early in my career I was fortunate to be part of a project with domain driven design (DDD) elements to its architecture. The senior engineers ensured the core was expressed while also abstracting data access and interaction with secondary systems. The code developed its own form of notation - a DSL (domain specific language). And this wasn't simply for the convenience of having common acronyms to share between contributors. The code structure also provided some shielding to new contributors (such as myself) which only allowed certain ways to interact with core functions. Part of that was enforced by a customized compiler and part was in group-managed coding standards. And of course the goal was to improve quality and reduce the chance of unwanted behaviors in a highly complex system. And over the years I've come to see nearly every large project filtered through that experiential lens. As Ardalis outlines above, whether you're using an object mapper like Entity Framework or a service interface via REST or GraphQL they serve as a burden and shield by abstracting away complexity that developers should only have to address when needed.
General Theory of Low Code Relativity
One of the exercises that brought this home was a recent live stream by Aaron Stannard. I have been reviewing Akka.NET and have been really taken with pthe actor model](getakka.net/articles/intro/what-problems-do..). He spent some time reviewing underlying .NET system libraries in the course of his own optimization for ActorPath Uri parsing. While I view Akka.NET code from a high level, I appreciate having the context for those situations where understanding it more deeply will shed light on the systems I build. But what was really interesting was that Aaron was doing the same thing as he went spelunking through the .NET system libraries - both checking for guideposts and giving a guided tour to those of us watching the live stream.
It really brought home that "low code" is in the eye of the beholder. And more importantly, the body of work that surrounded what Aaron was focused on was definitely not low code. There were functional proofs and performance benchmarks that were curated over months and years which provided a solid boundary around what he was working on. It was a substantial body of work that - once he had honed in on the changes he wanted to make - quickly revealed the improvements he had made in performance. This doesn't happen by accident, and it's certainly less likely in something labeled as "low code" as an eye-catching marketing term. As Aaron was working his way through .NET system library files he remarked in passing how there so much more to it than one would expect, and for what it's worth that's precisely what I thought when I was looking at the Akka.NET code base.
Sidebar: A Function In Three Acts
To illustrate how low code is all relative, here's an example processed through SharpLab.io of a sample array processing function written in F# which is then converted to C# and Intermediate (the pre-compiled code before it is converted to machine code). 5 instructions in F# becomes 88 lines of C#. And the same F# converts to 198 lines of IL. No matter how you choose to view this exercise, it's plain to see that there's (potentially) a great deal more going on under the hood when a high level instruction is issued.
let oneBigArray = [| 0 .. 100000 |]
// do some CPU intensive computation
let rec computeSomeFunction x =
if x <= 2 then 1
else computeSomeFunction (x - 1) + computeSomeFunction (x - 2)
// Do a parallel map over a large input array
let computeResults() = oneBigArray |> Array.Parallel.map (fun x -> computeSomeFunction (x % 20))
printfn "Parallel computation results: %A" (computeResults())
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using <StartupCode$_>;
using Microsoft.FSharp.Collections;
using Microsoft.FSharp.Core;
[assembly: FSharpInterfaceDataVersion(2, 0, 0)]
[assembly: AssemblyVersion("0.0.0.0")]
[CompilationMapping(SourceConstructFlags.Module)]
public static class @_
{
[CompilationMapping(SourceConstructFlags.Module)]
public static class ParallelArrayProgramming
{
[Serializable]
internal sealed class computeResults@11 : FSharpFunc<int, int>
{
[CompilerGenerated]
[DebuggerNonUserCode]
internal computeResults@11()
{
}
public override int Invoke(int x)
{
return computeSomeFunction(x % 20);
}
}
[CompilationMapping(SourceConstructFlags.Value)]
public static int[] oneBigArray
{
get
{
return $_.oneBigArray@3;
}
}
[CompilationMapping(SourceConstructFlags.Value)]
internal static PrintfFormat<FSharpFunc<int[], Unit>, TextWriter, Unit, Unit> format@1
{
get
{
return $_.format@1;
}
}
public static int computeSomeFunction(int x)
{
if (x <= 2)
{
return 1;
}
return computeSomeFunction(x - 1) + computeSomeFunction(x - 2);
}
public static int[] computeResults()
{
return ArrayModule.Parallel.Map(new computeResults@11(), oneBigArray);
}
}
}
namespace <StartupCode$_>
{
internal static class $_
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static readonly int[] oneBigArray@3;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static readonly PrintfFormat<FSharpFunc<int[], Unit>, TextWriter, Unit, Unit> format@1;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
[DebuggerNonUserCode]
internal static int init@;
static $_()
{
oneBigArray@3 = SeqModule.ToArray(Operators.CreateSequence(Operators.OperatorIntrinsics.RangeInt32(0, 1, 100000)));
format@1 = new PrintfFormat<FSharpFunc<int[], Unit>, TextWriter, Unit, Unit, int[]>("Parallel computation results: %A");
PrintfModule.PrintFormatLineToTextWriter(Console.Out, @_.ParallelArrayProgramming.format@1).Invoke(@_.ParallelArrayProgramming.computeResults());
}
}
}
.class private auto ansi '<Module>'
extends [mscorlib]System.Object
{
} // end of class <Module>
.class public auto ansi abstract sealed _
extends [mscorlib]System.Object
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = (
01 00 07 00 00 00 00 00
)
// Nested Types
.class nested public auto ansi abstract sealed ParallelArrayProgramming
extends [mscorlib]System.Object
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = (
01 00 07 00 00 00 00 00
)
// Nested Types
.class nested assembly auto ansi sealed serializable beforefieldinit computeResults@11
extends class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32, valuetype [System.Private.CoreLib]System.Int32>
{
// Methods
.method assembly specialname rtspecialname
instance void .ctor () cil managed
{
.custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
.custom instance void [System.Private.CoreLib]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x2090
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32, valuetype [System.Private.CoreLib]System.Int32>::.ctor()
IL_0006: ret
} // end of method computeResults@11::.ctor
.method public strict virtual
instance valuetype [System.Private.CoreLib]System.Int32 Invoke (
valuetype [System.Private.CoreLib]System.Int32 x
) cil managed
{
// Method begins at RVA 0x2098
// Code size 10 (0xa)
.maxstack 8
IL_0000: ldarg.1
IL_0001: ldc.i4.s 20
IL_0003: rem
IL_0004: call valuetype [System.Private.CoreLib]System.Int32 _/ParallelArrayProgramming::computeSomeFunction(valuetype [System.Private.CoreLib]System.Int32)
IL_0009: ret
} // end of method computeResults@11::Invoke
} // end of class computeResults@11
// Methods
.method public specialname static
valuetype [System.Private.CoreLib]System.Int32[] get_oneBigArray () cil managed
{
// Method begins at RVA 0x2050
// Code size 6 (0x6)
.maxstack 8
IL_0000: ldsfld valuetype [System.Private.CoreLib]System.Int32[] '<StartupCode$_>.$_'::oneBigArray@3
IL_0005: ret
} // end of method ParallelArrayProgramming::get_oneBigArray
.method public static
valuetype [System.Private.CoreLib]System.Int32 computeSomeFunction (
valuetype [System.Private.CoreLib]System.Int32 x
) cil managed
{
// Method begins at RVA 0x2058
// Code size 24 (0x18)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.2
IL_0002: bgt.s IL_0006
IL_0004: ldc.i4.1
IL_0005: ret
IL_0006: ldarg.0
IL_0007: ldc.i4.1
IL_0008: sub
IL_0009: call valuetype [System.Private.CoreLib]System.Int32 _/ParallelArrayProgramming::computeSomeFunction(valuetype [System.Private.CoreLib]System.Int32)
IL_000e: ldarg.0
IL_000f: ldc.i4.2
IL_0010: sub
IL_0011: call valuetype [System.Private.CoreLib]System.Int32 _/ParallelArrayProgramming::computeSomeFunction(valuetype [System.Private.CoreLib]System.Int32)
IL_0016: add
IL_0017: ret
} // end of method ParallelArrayProgramming::computeSomeFunction
.method public static
valuetype [System.Private.CoreLib]System.Int32[] computeResults () cil managed
{
// Method begins at RVA 0x2074
// Code size 18 (0x12)
.maxstack 8
IL_0000: newobj instance void _/ParallelArrayProgramming/computeResults@11::.ctor()
IL_0005: call valuetype [System.Private.CoreLib]System.Int32[] _/ParallelArrayProgramming::get_oneBigArray()
IL_000a: tail.
IL_000c: call !!1[] [FSharp.Core]Microsoft.FSharp.Collections.ArrayModule/Parallel::Map<valuetype [System.Private.CoreLib]System.Int32, valuetype [System.Private.CoreLib]System.Int32>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0, !!1>, !!0[])
IL_0011: ret
} // end of method ParallelArrayProgramming::computeResults
.method assembly specialname static
class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> get_format@1 () cil managed
{
// Method begins at RVA 0x2088
// Code size 6 (0x6)
.maxstack 8
IL_0000: ldsfld class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> '<StartupCode$_>.$_'::format@1
IL_0005: ret
} // end of method ParallelArrayProgramming::get_format@1
// Properties
.property valuetype [System.Private.CoreLib]System.Int32[] oneBigArray()
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = (
01 00 09 00 00 00 00 00
)
.get valuetype [System.Private.CoreLib]System.Int32[] _/ParallelArrayProgramming::get_oneBigArray()
}
.property class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> format@1()
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags) = (
01 00 09 00 00 00 00 00
)
.get class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> _/ParallelArrayProgramming::get_format@1()
}
} // end of class ParallelArrayProgramming
} // end of class _
.class private auto ansi abstract sealed '<StartupCode$_>.$_'
extends [mscorlib]System.Object
{
// Fields
.field assembly static initonly valuetype [System.Private.CoreLib]System.Int32[] oneBigArray@3
.custom instance void [System.Private.CoreLib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Private.CoreLib]System.Diagnostics.DebuggerBrowsableState) = (
01 00 00 00 00 00 00 00
)
.field assembly static initonly class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> format@1
.custom instance void [System.Private.CoreLib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Private.CoreLib]System.Diagnostics.DebuggerBrowsableState) = (
01 00 00 00 00 00 00 00
)
.field assembly static int32 init@
.custom instance void [System.Private.CoreLib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Private.CoreLib]System.Diagnostics.DebuggerBrowsableState) = (
01 00 00 00 00 00 00 00
)
.custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
.custom instance void [System.Private.CoreLib]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = (
01 00 00 00
)
// Methods
.method private specialname rtspecialname static
void .cctor () cil managed
{
// Method begins at RVA 0x20a4
// Code size 69 (0x45)
.maxstack 5
IL_0000: ldc.i4.0
IL_0001: ldc.i4.1
IL_0002: ldc.i4 100000
IL_0007: call class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<valuetype [System.Private.CoreLib]System.Int32> [FSharp.Core]Microsoft.FSharp.Core.Operators/OperatorIntrinsics::RangeInt32(valuetype [System.Private.CoreLib]System.Int32, valuetype [System.Private.CoreLib]System.Int32, valuetype [System.Private.CoreLib]System.Int32)
IL_000c: call class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<!!0> [FSharp.Core]Microsoft.FSharp.Core.Operators::CreateSequence<valuetype [System.Private.CoreLib]System.Int32>(class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_0011: call !!0[] [FSharp.Core]Microsoft.FSharp.Collections.SeqModule::ToArray<valuetype [System.Private.CoreLib]System.Int32>(class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_0016: stsfld valuetype [System.Private.CoreLib]System.Int32[] '<StartupCode$_>.$_'::oneBigArray@3
IL_001b: ldstr "Parallel computation results: %A"
IL_0020: newobj instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, valuetype [System.Private.CoreLib]System.Int32[]>::.ctor(class [System.Private.CoreLib]System.String)
IL_0025: stsfld class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> '<StartupCode$_>.$_'::format@1
IL_002a: call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()
IL_002f: call class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit> _/ParallelArrayProgramming::get_format@1()
IL_0034: call !!0 [FSharp.Core]Microsoft.FSharp.Core.PrintfModule::PrintFormatLineToTextWriter<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>>(class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [System.Private.CoreLib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
IL_0039: call valuetype [System.Private.CoreLib]System.Int32[] _/ParallelArrayProgramming::computeResults()
IL_003e: callvirt instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<valuetype [System.Private.CoreLib]System.Int32[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)
IL_0043: pop
IL_0044: ret
} // end of method $_::.cctor
} // end of class <StartupCode$_>.$_
"Low Code" is not an escape hatch that allows you to skirt past technical considerations. If anything, it actually increases the cognitive load as the designer uses high-level controls to enact a detailed business process in software.
R U Serious?
I mention in my COVID Datapothecary post of how I view R as a domain specific language, which may be met with some controversy in the community. Be that as it may, it's a great example of how it attracts a high number of non-developers from academia, and is a top choice for statistical modeling and machine learning. From that, R benefits from the inherent context of the constituency that provides a form of natural "guide rails" - most folks who come to R already have some background to approach the math involved in their domain, and are simply looking for a programmatic means to express it. And because of the broad package support, both for data processing and for operating in specific disciplines, there's a great deal of well-trod ground for new users to start their journey. And with that momentum the R ecosystem continues to grow apace.
I'm re-using the example below from another post but I want to repeat it here to illustrate how this function not only masks its underlying complexity but also protects the user "at the boundaries". Consider the following single line of code. It processes a column of data (here it's "new_cases") and generates a new one (ncrm - my short-hand for "new cases rolling mean") as part of a chain of functions against a source data set.
mutate(ncrm = slider::slide_dbl(new_cases, mean, .before = 3, .after = 3))
Below is the formula that this function expresses, and worth noting how the two look nothing alike. Above we're simply trusting that it will do the right thing - which is to take value at a given position, look "before" and "after" three positions and provide the mean for the declared range.
And it will perform this function as long as there are values in the named source column. Whether it's one or one billion values it will keep politely stepping through and performing the calculation until it's completed, and then attach the newly created range of values to the data frame. That's a bunch of work done by a single line of code.
This seems straight-forward enough, but what about the boundaries? What about the first position where there's no "before"? Well the function handles it properly. The same happens when it steps close to the end of the series. Everything is handled in a way that prevents brittle edge cases from surfacing. The users should definitely read the package documentation to understand how it handles those edge cases (such as empty versus null values - where I had substituted in 0 for any slot with a null or empty value in a previous function in the chain) and once understood it's a succinct and powerful tool. So that new column can now be plotted as a line along with the bars representing the individual daily aggregates, and we have a view of the data that's become all-to-familiar on the nightly news.
r blogdown::shortcode("fancybox", "/img", "data_originalCOVID_ncRawAndRM_NA.png")
The point here is to illustrate that there certainly is a way to build an algorithm in nearly any language that can calculate a rolling mean. But the ecosystem around R that makes it a particularly adept low-code environment for this type of task.
Check Your Wallet and Your Calendar
While R is specific, it's not that special in this regard. Almost any popular language family has this kind of ecosystem around it. Rust, Java, Python, Node, JavaScript, .NET and many others have their own ecosystem, whether the components are called library, package, crate, extension or some of the term-of-art. And if anyone is offering you a shortcut that skips past the context needed to account for its full cost, check your wallet and your calendar. Because even "free" low code solutions have costs - lock-in, lack of refactor-ability and most critically - time. The key takeaway here is that real low code is ubiquitous, and whether you keep it old school or go for the new shiny thing on display, you still must deal honestly with the complexities of the domain that surrounds your business. And you can't whistle past that fact like marketers can do with a happy path demo.
Low code is dead. Long live low code!