[HN Gopher] The Law of Leaky Abstractions (2002)
___________________________________________________________________
The Law of Leaky Abstractions (2002)
Author : skm
Score : 68 points
Date : 2024-03-06 19:47 UTC (3 hours ago)
(HTM) web link (www.joelonsoftware.com)
(TXT) w3m dump (www.joelonsoftware.com)
| Bostonian wrote:
| Should have (2002) in title.
| simonw wrote:
| I love this essay so much. I read it 22 years ago and it's been
| stuck in my mind ever since: it taught me that any time you take
| on a new abstraction that you don't understand, you're
| effectively taking on mental debt that is likely to come due at
| some point in the future.
|
| This has made me quite a bit more cautious about the abstractions
| I take on: I don't have to understand them fully when I start
| using them, but I do need to feel moderately confident that I
| could understand them in depth if I needed to.
|
| And now I'm working with LLMs, the most opaque abstraction of
| them all!
| Legend2440 wrote:
| >And now I'm working with LLMs, the most opaque abstraction of
| them all!
|
| You put a black box around it to fit it into the world of
| abstractions that traditional programs live in.
|
| But I'd say the most interesting thing about neural networks is
| that they do not have any abstractions within them. They're
| programs, but programs created by an optimization algorithm
| just turning knobs to minimize the loss.
|
| This creates very different kinds of programs - large, data-
| driven programs that can integrate huge amounts of information
| into their construction. It's a whole new domain with very
| different properties than traditional software built out of
| stacked abstractions.
| avgcorrection wrote:
| > Back to TCP. Earlier for the sake of simplicity I told a little
| fib, and some of you have steam coming out of your ears by now
| because this fib is driving you crazy. I said that TCP guarantees
| that your message will arrive. It doesn't, actually. If your pet
| snake has chewed through the network cable leading to your
| computer, and no IP packets can get through, then TCP can't do
| anything about it and your message doesn't arrive.
|
| The argument is disqualified at this point. The whole world is a
| leaky abstraction because <freak meteor hit could happen>. At
| this point your concept is all-encompassing and in turn useless.
|
| There are assumptions: this computation will finish eventually
| [assuming that no one unplugs the computer itself]. This does not
| make things leaky.
|
| There are leaky abstractions I guess but not all are. A garbage
| collector that can cause memory errors would be leaky. I don't
| know anything about garbage colletors but in my experience they
| don't.
|
| Then someone says that a garbage collector is leaky because of
| performance concerns (throughput or latency). That's not a leak:
| that's part of the _abstracting away_ part--some concerns are
| _abstracted away_. To abstract away means to make it something
| that you can't fudge or change. To say that "this is
| implementation-defined". An abstract _list_ is an abstraction in
| the sense that it has some behavior. And also in the sense that
| it doesn't say _how_ those behaviors are implemented. That's both
| a freedom and a lurking problem (sometimes). Big reallocation
| because of amortized _push_? Well you abstracted that away so can
| you complain about it? Maybe your next step is to move beyond the
| abstraction and into the more concrete.
|
| What are abstractions without something to abstract away? They
| are impossible. You have to have the freedom to leave some things
| blank.
|
| So what Spolsky is effectively saying is that abstractions are
| abstractions. That looks more like a rhetorical device than a new
| argument. (Taxes are theft?)
|
| EDIT: Flagged for an opinion? Very well.
| mjw_byrne wrote:
| I tend to agree. "All nontrivial abstractions are leaky"
| reminds me of other slightly-too-cute rules, such as "full
| rewrites are a mistake" and "never parse JSON manually".
|
| I wouldn't call TCP leaky because it can't deliver data across
| a broken network cable, for example. It's abstracting away
| certain unreliable features of the network, like out of order
| delivery of packets. It's not abstracting away the fact that
| networking requires a network.
| bxparks wrote:
| I unflagged you by vouching for you. I found your post
| difficult to understand and couldn't figure out what you are
| trying to say, but I agree it was not deserving of a flag.
| avgcorrection wrote:
| What was difficult about what I wrote?
| lpapez wrote:
| I don't agree with your opinion, but I see how it can be seen
| as being perfectly reasonable and there really is no need to
| flag you (unflagged).
| joeyjojo wrote:
| I suppose it should be considered where the abstraction
| actually exists. If the abstraction exists in logic or
| mathematics (ie. a triangle is a 3 sided polygon) it probably
| doesn't make much sense to consider the ramifications that
| thought occurs in a physical brain that can fail. On the other
| hand if the abstraction is physical (ie, hardware), then the
| fact that it is bound by physical law is obviously implicit.
| Software encompasses both physical and logical abstractions, so
| you need to pick a lens or perspective in order to actually
| view its abstractions.
| samatman wrote:
| > _There are leaky abstractions I guess but not all are. A
| garbage collector that can cause memory errors would be leaky.
| I don't know anything about garbage collectors but in my
| experience they don't._
|
| Garbage collectors are a rich source of abstraction leaks,
| depending on what you do with the runtime. If you color within
| the lines, no surprises, the garbage collector will work.
| Unless it has a bug, and hundreds of GC bugs, if not thousands,
| have shipped over the decades; but while a bug is an
| abstraction leak, it's not a very interesting one.
|
| But go ahead and use the FFI and things aren't so rosy. Usually
| the GC can cooperate with allocated memory from the other side
| of the FFI, but this requires care and attention to detail, or
| you get memory bugs, and just like that, you're manually
| managing memory in a garbage collected language, and you can
| segfault on a use-after-free just like a Real Programmer. It's
| also quite plausible to write a program in a GC language which
| leaks memory, by accidentally retaining a reference to
| something which you thought you'd deleted the last reference
| to. Whether or not you consider this an abstraction leak
| depends on how you think of the GC abstraction: if you take the
| high-level approach that "a GC means you don't have to manage
| memory" (this is frequently touted as the benefit of garbage
| collection), sooner or later a space leak is going to bite you.
|
| Then there are finalizers. If there's one thing which really
| punctures a hole in the GC abstraction, it's finalizers.
| evanmoran wrote:
| If you like this, my other favorite essay by Joel is Making Wrong
| Code Look Wrong:
|
| https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
| o11c wrote:
| The problem is that it completely ignores the correct solution,
| which is "use types; we invented them for a reason".
|
| HTML fragments should _never_ be stored in strings.
| mason55 wrote:
| One problem is that there's a major lack of language support
| to make this easy.
|
| IMO, every ID should be its own type. You shouldn't have a
| bunch of objects that have an ID of type string, you should
| have a User object with an ID of type UserID and a Post
| object with ID of type PostID, and then the compiler solves a
| lot of problems for you. Or make it so your functions that
| interact with the outside world accept a String, but they
| only return ValidatedStrings, and your internals only accept
| ValidatedStrings (and there's only one way to turn a String
| into a ValidatedString).
|
| But in any kind of language with structural typing (e.g.
| TypeScript) this doesn't work, by definition. You can call a
| string a UserID and you can call it a PostID but if they're
| both Strings then you can assign them to each other.
|
| And in Java, the concept of a typedef-like operation doesn't
| exist at all (I can't speak for .Net).
|
| There's a whole class of bugs that go away if you allow for
| easy, nominal typedefs, but it's actually not easy to do in
| most statically typed languages.
| mjw1007 wrote:
| I never liked the way he used TCP as an example here.
|
| I don't think it's sensible to think of "make it reliable" as a
| process of abstraction or simplification (it's obviously not
| possible to build a reliable connection on top of IP if by
| "reliable" you mean "will never fail"). "You might have to cope
| with a TCP connection failing" doesn't seem to be the same sort
| of thing as his other examples of leaky abstractions.
|
| TCP's abstraction is more like "I'll either give you a reliable
| connection or a clean error". And that one certainly does leak.
| He could have talked about how the checksum might fail to be
| sufficient, or how sometimes you have to care about packet
| boundaries, or how sometimes it might run incredibly slowly
| without actually failing.
| joe_the_user wrote:
| Indeed, his discussion seems to involve a confusing of a leaky
| network protocol and a leaky abstraction. Perhaps he wanted to
| meta-illustrate his concept by having his discussion itself be
| leaky.
| lcuff wrote:
| I like the idea of TCP as a leaky abstraction because it points
| out the difficulty of engineering the abstraction we really
| want. It would be wonderful for TCP to be a guaranteed
| connection abstraction, but it turns out in today's world, the
| abstraction of a reliable connection is TCP + a network
| administrator + a guy with wire snips + solder
| (metaphorically). Maybe down the road, AIs and repair bots will
| be involved, and the guaranteed connection abstraction might
| become real or much much stronger. Although it gets more
| complicated because if a message takes hours to deliver, is
| that going to work for your application? Yes if you're
| archiving documents, no if you're trying to set up a video
| conference call or display a web page.
|
| TCP is problematic in modern circumstances (think: Inside a
| data center) because a response within milliseconds is what's
| expected to make the process viable. TCP was designed to
| accommodate some element of the path being a 300 Baud modem,
| where a response time in seconds is possible as the modem dials
| the next hop, so the TCP timeouts are unuseable. QUIC was
| developed to address this kind of problem. My point being, the
| abstraction of a guaranteed _timely_ connection is even harder.
|
| I think Joel could have expanded his thoughts to include the
| degree of leak. SQL is a leaky abstraction itself, yes, but my
| own take is that ORMs are much leakier: Every ORM introduction
| document I've read explains the notation by saying "here's the
| sql that is produced". I think of ORMs as not a bucket with
| holes, but a bucket with half the bottom removed.
| anonymous-panda wrote:
| > but it turns out in today's world, the abstraction of a
| reliable connection is TCP + a network administrator + a guy
| with wire snips + solder (metaphorically).
|
| I think you've misunderstood the abstraction. In fact, TCP is
| not leaky because there's wire snips or cable cuts. In fact,
| BGP will route around physical failures. But aside from that,
| it abstracts all the various failure modes as a single
| disconnection error. A leaky abstraction would be when you
| need to still distinguish the error type and TCP wouldn't let
| you. A 100% reliable connection is physically impossible in
| any context (and an intrinsic concept of distributed systems
| which every abstraction is leaky over including the CPU bus)
| so if that's your bar then all tech will be a leaky
| abstraction. It is at some level but not in a way that's
| helpful to have a fruitful discussion.
| BoiledCabbage wrote:
| Young people should probably know that (as far as I recall) Joel
| more or less invented tech blogging as a form of
| advertising/recruiting for your company.
|
| Namely either listing out the process/perks that a good
| engineering team should have and how conveniently his company has
| it. Or describing interesting and challenging problems they
| solved and how you can join them and solve problems like that
| too.
|
| I don't recall anyone popular doing it before him and it's pretty
| much industry standard now. (Although, feel free to chime in if
| that's wrong. But popular being a key word here),
| williamcotton wrote:
| Cannot everyone get the sense that how we currently build
| software is one gigantic leaky abstraction?
| Legend2440 wrote:
| Worse; it's a stack of abstractions on top of abstractions on
| top of abstractions. You're at least 10 layers away from the
| hardware, possibly more.
| williamcotton wrote:
| I don't necessarily see that as the problem. Assembly is a
| great abstraction over machine code. Languages that compile
| to these bytecodes are a good abstraction. Garbage collected
| languages are a good abstraction.
|
| Web applications are not a good abstraction. Auth, storage,
| route handlers, tests, deployment, et al, are all cobbled
| together like chocolate ice cream and jalapenos on an
| uncooked bed of salmon and root beer.
| Veserv wrote:
| Every abstraction leaks. A good abstraction for your domain is
| stable in your domain and only leaks outside of your domain. A
| great abstraction is separable allowing you to only drop down
| the abstraction level where needed and allowing the rest of the
| code to continue using the abstraction where the leaks do not
| matter, and layered allowing you to only drop down as much as
| needed and making it easy to rebuild parts of the upper layers
| on a new foundation.
| highfrequency wrote:
| > "All abstractions leak, and the only way to deal with the leaks
| competently is to learn about how the abstractions work and what
| they are abstracting. So the abstractions save us time working,
| but they don't save us time learning."
|
| Very nicely worded. But I would also add that:
|
| 1. An abstraction can often be manned by one person, so when it
| leaks only one person needs to understand it deeply enough to fix
| it.
|
| 2. The article seems to miss the _iterative_ nature of
| abstractions. Over time, the goal is to iterate on the
| abstraction so that it exposes more of the stuff that matters,
| and less of the stuff that doesn't matter. Perhaps all
| abstractions leak, but some leak way less often and save much
| more thinking in the meantime than others. Rather than lamenting
| the nature of abstractions we should focus effort on making them
| as practically useful as possible.
| glial wrote:
| This is well-written. I might suggest that what makes pure
| mathematics special is that abstractions in pure math are not
| leaky, unlike in (nearly?) every other domain.
| mturmon wrote:
| Didn't downvote, don't entirely disagree, but maybe it would be
| OK to say that the leaks can be made more apparent:
|
| "The integral reverses the derivative" + ++ *
|
| + Up to an arbitrary additive constant
|
| ++ Provided the derivative exists
|
| * And we hope you don't have concerns about the existence of
| the real numbers
| an1sotropy wrote:
| I first learned about "leaky abstractions" from John Cook, who
| describes* IEEE 754 floats as a leaky abstraction of the reals. I
| think this is a good way of appreciating floating point for the
| large group of people who's experience is somewhere between
| numerical computing experts (who look at every arithmetic
| operation through the lens of numerical precision) and total
| beginners (who haven't yet recognized that there can't be a one-
| to-one correspondence between a point on the real number line and
| a "float").
|
| * https://www.johndcook.com/blog/2009/04/06/numbers-are-a-leak...
| cloogshicer wrote:
| I've long been having a hunch that we're currently in the "wild
| west of abstraction".
|
| I think we're missing an essential constraint on the way we do
| abstraction.
|
| My hunch is that this constraint should be that abstractions
| _must_ be reversible.
|
| Here's an example: When you use a compiler, you can work at a
| higher layer of abstraction (the higher-level language). But,
| this means you're now _locked into_ that layer of abstraction. By
| that I mean, you can no longer work at the lower layer
| (assembly), even if you wanted to. You could in theory of course
| modify the compiler output after it 's been generated, but then
| you'd have to somehow manually keep that work in sync whenever
| you want to re-generate. Using an abstraction kinda locks you
| into that layer.
|
| I see this problem appearing everywhere:
|
| - Use framework <--> Write from scratch
|
| - Use an ORM <--> Write raw SQL
|
| - Garbage collection <--> Manual memory management
|
| - Using a DSL <--> Writing raw language code
|
| - Cross platform UI framework <--> Native UI code
|
| - ...
|
| I think we're missing a fundamental primitive of abstraction that
| allows us to work on _each layer_ of abstraction without being
| locked in.
|
| If you have any thoughts at all on this, please share them here!
| ihumanable wrote:
| Lots of abstractions have an escape hatch down to the lower
| level, you can put assembly in your C code, most ORMs have some
| way to just run a query, etc.
|
| I think the question I have is, what benefit does this provide?
| Let's say we could wave a magic wand and you can operate at any
| layer of abstraction. Is this beneficial in some way? The
| article is about leaky abstractions and states
|
| > One reason the law of leaky abstractions is problematic is
| that it means that abstractions do not really simplify our
| lives as much as they were meant to.
|
| I think I'm just struggling to understand how this would help
| with that.
| cloogshicer wrote:
| It would help because you could tackle the problem at hand
| always at the right layer of abstraction.
|
| If a certain aspect of the problem can be solved easily in a
| higher layer of abstraction, great! Let's solve it at that
| layer, because it's usually easier and allows for more
| expressiveness.
|
| But whenever we need more control, we can seamlessly drop
| down to the lower layer and work there.
|
| I think we need to find a fundamental principle that allows
| this. But I see barely anyone working on this - instead we
| keep trying to find higher and higher layers of abstractions
| (LLMs being the most recent addition) in the hopes they will
| get rid of the need of dealing with the lower layers. Which
| is a false hope, I feel.
| titzer wrote:
| I work on programming languages and systems (virtual machines).
| A key thing with a systems programming language is that you
| need to be able to do things at the machine level. Here's a
| talk I gave a year ago about it:
| https://www.youtube.com/watch?v=jNcEBXqt9pU
| eschneider wrote:
| That's not really true of, at least C compilers. Because
| compilers have ABI's and fixed calling conventions, it's
| straightforward, documented, and not uncommon (depending on
| your application area/deployment target) to drop down to the
| ASM layer if you need to do that.
|
| It's definitely one of those things that makes C nice for bare
| metal programming.
| cloogshicer wrote:
| Interesting, I'm curious though, once you do drop down to the
| ASM layer, how do you ensure that this code doesn't get
| overwritten by new compiler output? Or is this something you
| somehow include in the compile step?
| zro wrote:
| In my experience (admittedly limited) you include the
| assembly in the compile step. Your linker hopefully puts
| everything together so you can talk to yourself
| Veserv wrote:
| No, reversible abstractions are just one kind of abstraction.
| For instance, a machine code sequence to a linear sequence of
| assembly instructions is a reversible abstraction. Not every
| machine code sequence is expressible as a linear sequence of
| assembly instructions, but every linear sequence of assembly
| instructions has a trivial correspondence to a machine code
| sequence.
|
| However, consider the jump to a C-like language. The key
| abstraction provided there is the abstraction of infinite local
| variables. The compiler manages this through a stack, register
| allocation, and stack spilling to provide the abstraction and
| consumes your ability to control the registers directly to
| provide this abstraction. To interface at both levels
| simultaneously requires the leakage of the implementation
| details of the abstraction and careful interaction.
|
| What you can do easily is what I call a separable abstraction,
| a abstraction that can be restricted to just the places it is
| needed/removed where unneeded. In certain cases in C code you
| need to do some specific assembly instruction, sequence, or
| even function. This can be easily done by writing a assembly
| function that interfaces with the C code via the C ABI. What is
| happening there is that the C code defines a interface allowing
| you to drop down or even exit the abstraction hierarchy for the
| duration of that function. The ease of doing so makes C highly
| separable and is part of the reason why it is so easy to call
| out to C, but you hardly ever see anybody calling out to say
| Java or Haskell.
|
| Of course, that is just one of the many properties of
| abstractions that can make them easier to use, simpler, and
| more robust.
| jerf wrote:
| Abstractions work by restricting the domain of what you can do,
| then building on those restrictions. For example, raw hardware
| can jump anywhere, but structured programming constrains you to
| jump only to certain locations in order to implement if, for,
| functions, etc. It is precisely those restrictions that bring
| the benefits of structured programming; if you still frequently
| dipped into jumping around directly structured programming
| would fail to provide the guarantees it is supposed to provide.
| CRUD frameworks provide their power by restricting you to CRUD
| operations, then building on that. Immutable data is
| accomplished by forbidding you from updating values even though
| the hardware will happily do it. And so on.
|
| Escape hatches under the abstractions are generally there
| precisely to break the abstractions, and break them they do.
|
| Abstractions _necessarily_ involve being irreversible, or, to
| forestall a tedious discussion of the definition of
| "irreversible", necessarily involve making it an uphill journey
| to violate and go under the abstraction. There's no way around
| it. Careful thought can make using an escape hatch less pain
| than it might otherwise be (such as the ORM that makes it
| virtually impossible to use SQL by successfully hiding
| everything about the SQL tables from you so you're basically
| typing table and column names by dead reckoning), but that's
| all that can be done.
|
| One thing to do about this is that just as in the past few
| years the programming community has started to grapple with the
| fact that libraries aren't free but come with a certain cost
| that really adds up once you're pulling in a few thousand
| libraries for a framework's "hello world", abstractions that
| look really useful but whose restrictions don't match your
| needs need to be looked at a lot more closely.
|
| I had something like that happen to me just this week. I needed
| a simple byte ring buffer. I looked in my language's repos for
| an existing one. I found them. But they were all _super_
| complicated, offering tons of features I didn 't need, like
| being a writethrough buffer (which involved taking restrictions
| I didn't want), or where the simple task of trying to
| understand the API was quite literally on par with implementing
| one myself. So I just wrote the simple thing. (Aiding this
| decision is that broadly speaking if this buffer does fail or
| have a bug it's not terribly consequential, in my situation
| it's only for logging output and only effectively at a very
| high DEBUG level.) It wasn't worth the restrictions to build up
| stuff I didn't even want.
| cloogshicer wrote:
| > It is precisely those restrictions that bring the benefits
| [...]
|
| Wouldn't it be possible to say "ok, I'll take those
| restrictions as long as they benefit me, but once I notice
| that they no longer do, I'll break them and drop down to the
| lower layer. But only for those parts that actually require
| it"?
|
| > Abstractions necessarily involve being irreversible, or, to
| forestall a tedious discussion of the definition of
| "irreversible", necessarily involve making it an uphill
| journey to violate and go under the abstraction.
|
| Why? Not being snarky, I'm genuinely trying to understand
| this better.
| samatman wrote:
| > _Here 's an example: When you use a compiler, you can work at
| a higher layer of abstraction (the higher-level language). But,
| this means you're now locked into that layer of abstraction. By
| that I mean, you can no longer work at the lower layer
| (assembly), even if you wanted to._
|
| Native-code compilers commonly allow emitting assembly
| directly, but now your source code isn't portable between CPUs.
| Many interpreted languages, even most, allow FFI code to be
| imported, modifying the runtime accordingly, but now your
| program isn't portable between implementations of that
| language, and you have to be careful to make sure the behavior
| you've introduced doesn't mess with other parts of the system
| in unexpected ways.
|
| Generalizing, it's often possible to drill down beneath the
| abstraction layer, but there's often an inherent price to be
| paid, whether it be taking pains to preserve the invariants of
| the abstraction, losing some of the benefits of it, or both.
|
| There are better and worse versions of this layer, I would
| point to Lua as a language which is explicitly designed to
| cross the C/Lua boundary in both directions, and which did a
| good job of it. But nothing can change the fact that pure-Lua
| code simply won't segfault, but bring in userdata and it very
| easily can; the problems posed are inherent.
| wvenable wrote:
| Most ORMs give a way to integrate nicely with SQL if you need
| to reach down to that layer and still use the rest of the ORM
| features.
|
| There is no silver bullet; everything is a trade off. Almost
| all of the time, the trade off is entirely worth it even if
| that gets you locked into that solution.
| cloogshicer wrote:
| > Most ORMs give a way to integrate nicely with SQL if you
| need to reach down to that layer and still use the rest of
| the ORM features.
|
| Agreed, that's a good thing, in my experience.
|
| > Almost all of the time, the trade off is entirely worth it
| even if that gets you locked into that solution.
|
| I wish this would match my experience.
| wvenable wrote:
| What's funny is that ORMs giving you directly access to SQL
| is a leaky part of the abstraction but in this case that's
| good!
|
| I think being locked into a some abstraction is so common
| place that you don't even consider it being a thing until
| you have a problem with it. Look at your examples:
| compilers, libraries, frameworks, etc. As an example, is
| anyone truly upset that coding in Python locks you into the
| Python ecosystem? Yet, I've used libraries/frameworks that
| didn't win the war of popularity and I'm still
| unfortunately committed. I think there's a bias at play in
| how we look at these things.
| titzer wrote:
| Joel of course hits the nail on the head about the two major
| things that cause abstractions to fall apart: performance and
| bugs (or debugging). In programming languages we talk about
| abstractions all the time--PL of course is all about
| abstractions. A computational abstraction like a bytecode, source
| language, or even machine code, can be proven be a proper (or
| full) abstraction, meaning there is no way for implementation
| details to leak in--you cannot observe the electrons flowing by
| executing A+B, after all.
|
| ...until you start measuring sidechannels, or the CPU or compiler
| has a bug.
|
| I think about this a lot when dealing with VMs; a complex VM
| cannot hide its complexity when programs care about execution
| time, or when the VM actually has a bug.
| refactor_master wrote:
| A car is an implementation meant to deal with a problem (the
| weather), but never abstracts away physics or forces full buy-in
| to some alternate reality. You can't just go around and say any
| imperfection in an implementation is a leaky abstraction. That's
| not how it works.
|
| My shoe is not abstracting away the terrain, nor is it leaky
| because it doesn't handle _all_ weather conditions. Well, it is
| leaky, but not in that sense.
| samatman wrote:
| An analogy is an abstraction, and abstractions leak.
| wvenable wrote:
| I loved this essay when it came out but I've come to dislike how
| "leaky abstraction" has become a form of low effort criticism
| that gets applied to almost anything.
| davesque wrote:
| I feel like this article should be called "The Law of Bad
| Abstractions." I often see this cited as a blanket rejection of
| complexity in software. But complexity is unavoidable and even
| necessary. A skillful engineer will therefore design their
| abstractions carefully and correctly, balancing time spent
| thinking forward against time spent implementing a solution. I
| think Joel understands this, but it feels weird how he frames it
| as a "law", as though it's something he's discovered instead of a
| simple fact that arises from the nature of what abstractions are:
| things that stand in for (or mediate interaction with) some other
| thing without actually being that thing. What a surprise that the
| stand-in ends up not being the actual thing it's standing in for!
___________________________________________________________________
(page generated 2024-03-06 23:00 UTC)