On Emulation
Last week I said that I had some thoughts on "emulation" but hadn't yet hammered them out. So today I'm gonna hammer them out. I might eventually polish this into a blog post. We'll see.
Let's say you half a list of numbers xs
, and you want to apply some function to them all. How would you do that? Most of you would use map
, I'm guessing. Great! Now I take away higher-order functions: you can't pass functions into other functions or return them as values. Now you can't use map
anymore. But you do get late-binding objects. You've got a workaround:
# This is all pseudocode
interface Strategy[A, B]{
fun execute(A): B;
}
fun map(f: Strategy[A, B], xs: List[A]): List[B] {
out: List[B] = [];
for(x in xs) {
out.append(f.execute(x));
}
return out;
}
Just implement Strategy
, fill in execute
, and yer done! This trick was so common in early OOP that people called it the strategy pattern. Now there's a pretty clear relation between strategies and being able to pass in functions. We can say that the strategy pattern emulates functions as first class inputs. More generally, emulation is the process of using your language primitives to reconstruct another language's features.
Emulation does have some advantages. You could argue that the strategy pattern is in some ways "better" than passing in a function. The strategy can have internal state and do things like track how often it is called. You could have strategies inherit from other strategies for whatever reason. You could require all strategies to also define their setup and teardown, etc. And you'd be right: these are things that aren't necessarily easy to do with HOFs! Every language feature makes tradeoffs in its design space. Since you're reconstructing the feature, you could choose slightly different design parameters.
On the other hand, you could show all the ways the emulation fails. Like how you can't construct new strategies at runtime. Or how janky composing two strategies together is. Or how you need a separate interface for every strategy type signature. Or how you have tons of boilerplate for what should be a simple operation. If you're used to passing around functions, strategies will feel like a horrible kludge.
The problem with emulation is that language functionality is holistic. You are emulating a small facet of the language but not the features that support it, the tools that work with it, or the conscious limitations placed on it. Your starting language is also holistic: the emulation doesn't naturally fit into the broader context of what you already have. You're no longer on the happy path.
I was thinking of it in the topic of static vs dynamic types because "dynamic types are just static types with one type" is often realized as an emulation argument. You "add" dynamic types to a static language by using a Dyn
type for everything. And you can argue that this is better than having """real""" dynamic types. After all, you now have the dynamicism only where you need it. And you'd be right: you've successfully emulated dynamic typing in your static system.
But you're also working at cross-purposes with the static type system. Each function that uses Dyn
can 'infect' other functions with it to also use Dyn
, unless you are very careful. And you don't get the benefits of using a language designed for dynamic typing. It's going to be really hard to define equality between Dyn
values without leaving the emulation. There's probably ways of doing that, too, but then you'll generate more friction between the two worldviews.
Does this mean Dyn
is useless? Nope. It serves a purpose in the static language. But it means that the emulation isn't as rich as whatever you're emulating. As Alexis King puts it:¹
I’ve seen a few static typing fans advancing the “dynamically typed languages are like statically typed languages that have just one type” argument. This is sometimes a useful perspective to take, but it usually isn’t. Dynamically typed languages have different types of values.
— Alexis King (@lexi_lambda) January 20, 2020
I'm bringing up emulation in the context of type systems, because that's where I first hammered out the ideas, but tbh that doesn't seem like a great use of the idea. Here are some interesting aspects of emulation as an idea:
- When we decide to emulate a specific feature versus approach the problem in a different way. When Go users say they don't need generics because they have
interface{}
and code generators, that's not emulation, because they're not reconstructing generics. On the other hand, using Canadian Aboriginal syllabics is emulating generics. - When we're pushing it too far and should be using a different language.
- The ways we make the emulation "different" than the emulated.
- The frictions and limitations of emulation.
- How the community thinks about emulation. Lisp programmers used to feel very superior to other languages because they can easily emulate other langs, but emulation isn't the same as native features. I think language communities tend think that if they can emulate feature X, then their language is superior to one which natively has feature X.
¹ A lot of people dogpiled her for this. People can be jerks sometimes
If you're reading this on the web, you can subscribe here. Updates are once a week. My main website is here.
My new book, Logic for Programmers, is now in early access! Get it here.