declarative is a buzzword
Written: 2020-10-29 to 2020-10-30by Naomi Liu
I feel like I’ve been putting in way more effort into this site than it’s actually worth. I’ve spent close to 24 hours on the CSS in total, and it’s still somewhat broken on mobile and narrow screens.
If you haven’t noticed, the footer element is still avoiding the bottom of the viewport on short pages. I don’t know how to fix this, so I’m going to accept it as an aesthetic consequence for now. If anyone has any advice, please let me know.
To be honest, I see frontend development as a completely stochastic
and chaotic process - HTML
and CSS
are supposed to be declarative, but
there’s an insane margin of error. There’s no concrete way to debug CSS
sometimes other than manually clicking through properties in DevTools,
and even then, you’ll often find that the source of your bug was
somewhere completely unexpected. For instance, the nav bar you see above
you was broken for a few days in terms of indentation. I spent close to
20 minutes trying every possible permutation of options that could have
broken it, only to realize that my issue was setting it as an inline
element. There is zero intuitive indication that text-align
doesn’t apply directly to inline elements, but rather to inline elements
within a block element. It took me longer than I’m proud to admit to
figure this out.
The idea behind declarative languages is that you can write a
specification of your logic, trusting in the language’s implementation
to actually do what you have in mind. If I say there’s a
<p>
, there should be a paragraph. If I want
<p style="color: red">
, that <p>
had better be red. SQL
is declarative
too - if you wanna get the Users
table
from your database, filtered by age, you can reasonably expect that the
output will follow that specification. You don’t care about how the
filtering or search actually goes down. You especially don’t want to
care about whether your search will suddenly change the results of your
next search.
Websites don’t actually have to look good. It’s a social construct.
Any effort you put into your site is for yourself (unless you’re a
business or public-facing organization, in which case you’re a victim of
the masses). We deal with JS
and CSS
because they’re our only options for
developing websites - if I could just draw my layout in GIMP
and apply that directly, I’d be
elated.
I’m loving Hakyll
so far for how
easy it is to put this site together. I only really have to worry about
the CSS
- all these posts are written
in plain Markdown
, and converted to
HTML with Pandoc
. Jekyll
and a million other static site
generators do the same thing, but Hakyll
is way more entertaining to
configure. Any processing I want done on my files is written
declaratively, in the best way. If I want to specify a data-processing
step, it takes 3-4 lines at most, and I’m safe knowing that it’s not
going to break my site if I’m incorrect.
What’s great about Haskell? The type system. Particularly, for how
pedantic it is. Your code won’t compile if it sucks. Every compiler has
syntax checking to some extent (I’m looking at you GCC, with your
your garbage code makes a pointer from an integer without a cast
),
but you’re not protected from errant memory access or weird runtime
errors that pop up months in the future. About 99% of the time, your
oversights will be caught by the Haskell compiler before they even have
a chance to run. Forget to account for an edge-case when you’re pattern
matching? Try to add a JSON file to a number? It might happen, but it’ll
only happen in your text editor, instead of the runtime.
Haskell
is what immediately comes to
mind when I think of declarative style done right - you have the ability
to authoritatively define the information flow of your program, and as
long as it typechecks, your program will usually work. Your mental model
of your program changes from thinking about how to modify data and keep
it in a specific state, to thinking about what possible forms of data
can be represented in your code. For instance, reducing a collection of
values is as simple as using foldr
. You don’t need to think
about what the collection you’re folding is going to look like after you
reduce it, nor do you need to think about how it’s implemented. If
you’re just folding over lists, for all you care, it’s written as
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
Plain and simple, right? Did you know the actual implementation looks like this? (src)
foldr :: (a -> b -> b) -> b -> t a -> b
foldr f z t = appEndo (foldMap (Endo #. f) t) z
where t
refers to any Foldable
collection. I don’t even know what
this means. Something to do with endomorphisms and list fusion
optimizations.
It doesn’t matter. There’s zero need to document any possible way it might mangle your original collection, because it won’t. Referential transparency gives you a guarantee that you’ll be able to access and work with your original values, even after you apply a function to them. You’ll never dome your head on something like
> const arr = [1,2,3];
> arr.push(4);
> arr
1,2,3,4]
[
> arr.concat([5]);
> arr
1,2,3,4] [
where it’s unclear which methods carry side-effects, and which ones don’t. (please let me know if my Javascript rants get repetitive.)
I think that people tend to veer away from functional languages
because the type system appears intimidating at first. When you’re first
learning, it feels like you have some pedantic coworker learning over
your shoulder, quietly whispering Couldn't match expected type ‘Integer’ with actual type ‘Char’. You donkey.
No, it’s not quiet. It’s more like getting called out on stage for
daring to challenge the type checker’s status quo. But - when you start
to work with the type checker, learning to scan past the million
passive-aggressive lines of pedantry to the immediately meaningful
parts, you hit a profound realization of how these types can fit
together. The parametric polymorphism of Haskell's
type system also lets you write
completely generic code that work on structures, instead of values. You
can overload your operators all you want, and trust that they’ll be
valid as long as your values are defined within their domains.
Eventually, you stop seeing esoteric variable-type values as Integral a
or EitherT String LispVal
and start seeing
building blocks, whose interactions are subtle but clear. You realize
that you’re simply defining boundaries on data, rather than
outright transformations. Reading type signatures starts to feel like
reading music, because they’re a language of their own that describes
how your functions are allowed to combine. Dynamically-typed languages
start to feel cold, unfamiliar, and primitive, just like visiting
Saskatchewan. A fantastic tool that uses the power afforded by this
programming paradigm is Hoogle.
It’s a full search engine for Haskell libraries and functions, with one
key difference over other language databases: you can search for
functions using type signatures.
When you’re painting and you find yourself needing a specific shade of green, you rely on paint swatches to figure out a contrast between colours, and to reason about how the green might interact with the other hues on your canvas. When you’re programming in a functional style, you might need to figure out an adapter function for your values. Say you have a tuple of values, representing a pair of coordinates in space:
type Pos = (Float, Float)
and you want to transform them diagonally by 1 unit each. You could just write
northWest :: Pos -> Pos
= (a + 1, b + 1) northWest (a, b)
But: if you’re in the know, you can just pull up Hoogle (there are
plugins for Vim, Emacs, GHCi, and there’s a CLI program too!) and search
for the specific type signature that would accomplish what you’re trying
to do. You want a function that takes a function of type
(a -> b)
, that takes a tuple containing two values of
the same type, and returns a new tuple containing the results of
applying that function twice.
$ hoogle "(a -> b) -> (a, a) -> (b, b)"
Data.Tuple.Extra both :: (a -> b) -> (a, a) -> (b, b)
. . .
Awesome. Now you have a higher order function you can use to apply
any unary function to any homotypic tuple. You can also Hoogle
a function name if you forgot its type
signature, which I also make heavy use of. It’s like having a friendly
art store employee that knows exactly what shade of paint you need,
provided you can describe it.
Having static analysis as a safety net is immensely powerful - it restricts you in the sense that you’re kept from making stupid mistakes that break at the worst possible time, rather than restricting your creativity. It lets you abstract away more than repetitive code - you no longer have to care about trivial things like off-by-one errors, keeping track of pointers, wondering why people like JavaScript, etc. If only life was this simple.
Declarative languages in general are cool because they let you
abstract away from the rigmarole of telling a computer
how
to do something - you only need to specify
what
needs to be done. The problem is, the
possibility space of what can
be done in a
language’s context isn’t perfect, or even necessarily logical. It’s
arbitrary, and written by people: the archenemy of consistency. If
documentation is good, that saves you a lot of time and frustration in
figuring out why your description of a task isn’t working. I love Nix
for this - it’s a declarative language
that lets you specify the exact description of a system configuration,
and trust that any build with the same configuration will wind up the
same. The documentation is fantastic.
The opposite of Nix
is CSS
- it purports to be declarative, but half
of the advice you see online isn’t even part of the spec. They’re hacks,
meant to work around a system that’s simultaneously intuitive, and
aggravatingly confusing. For instance:
- leveraging comments to remove whitespace
- using
overflow:auto
to keep parents of float elements from collapsing
Reading through CSS
threads from a
time before inter-browser compatibility is even more entertaining. I
admire the sheer lengths people went to, in order to deal with
Microsoft. Hacks are a beautiful thing - they’re the computational
representation of human ingenuity. But for a language that purports to
be declarative, in the sense that “what you say is what you get”, CSS
has way too many idiosyncrasies you have
to work around.
CSS
documentation is spotty, and
there’s a ton of conflicting advice out there. I try to veer toward the
MDN docs because
they’re clear, consistent, and unobtrusive. w3schools on the other hand, feels
cluttered. The information is decently laid-out, but sometimes the
examples are obtuse and take a while to wrap my head around. The site
design is also surprisingly dated for a resource that’s aimed towards
teaching web developers. When you write declarative code, you need to
know what to declare. CSS
isn’t a programming language, so any hacks that you can accomplish are
built off other obscure, and non-deterministic settings. Compared to the
documentation of other declarative languages, CSS
feels less like having a well-organized
toolbox, and more like trying to find a screwdriver that’s buried
somewhere in a hoarder’s shed.
In my experience, I tend to run into a lot of problems in my CSS
that stem from unexpected inheritance. If
I add an indent property on my <body>
, that indent
will apply to any <code>
blocks within my body too.
That’s by design - your mistakes cascade down with style, leaving you
wanting to hide under your sheets for the rest of the day. Proper style
dictates that you should try to encapsulate your classes to restrict the
scope of any possible mistakes, but that kind of defensive programming
breaks down on larger projects. Why do you think a billion CSS
compilers exist? The first compilers were
written by lazy geniuses to avoid the pain of having to write assembly.
Today, we have tools like PureScript
, GHCJS
and less
to do the same thing -
abstract away the painful things.
There’s a cool package called css that lets you perform
static analysis on your CSS
. It parses
your stylesheet into an AST, and lets you perform transformations on the
tree that ripple downwards. With the flexibility gained in your CSS
manipulation, you would almost think that it was a meaningful language
or something.
I think that it would be amazing if there was a version of CSS out
there where illogical states couldn’t even be represented. In its
current form, most built-in properties have a specific domain of allowed
settings. word-wrap
can only ever be one of
[normal, break-word, initial, inherit]
. Any sane programmer
would implement these as enumerated data types. However, the only way
you’ll be able to catch an illegal value in CSS
is if you’re using a linter or if you
manually inspect the element yourself. The browser will still let your
site load, just fine. If you unscrupulously change a flex
element to a grid
, you might find that your entire
website’s width has grown past the boundaries of your viewport and
sanity. Any attempts to define a constant width
will be met
by an error, but not in your IDE or editor. You’ll have to manually seek
out your mistake by yourself. It’s like we’re still living in the
80’s.
And before you tell me to just use a site template, realize that the only modicum of JS on this site is in code examples. I already feel bad for embedding fonts and wasting people’s bandwidth.
Overall, CSS
is obviously designed
with redundancy and error-recovery in mind. When you have a language
where it’s so easy to make obscure mistakes, it’s a good thing that your
mistakes are forgiven. The problem is, this forgiveness isn’t
transparent. Rampant inheritance leads to side-effects and unexpected
consequences from your declarations. The point of declaration is to
express logic authoritatively, without having to worry about how your
code affects the state of a system. Writing CSS
feels like a weird struggle between you
and the browser, steadfast in its refusal to let you center that
<div>
. Obviously, this makes for a more reliable
internet. But all the hacks that proper web design requires, only
accumulates technical debt for the future. You can either try to write
it right, or right your writes. It’s a rite, right?