Simplicity vs Complexity

The wonderful thing about Keyboard Maestro is the sheer range of trade-offs, offered to a wide spectrum of users, in terms which kind of 'simplicity' we need, and where we sweep the complexity away to.

(Simplicity is like the cool in a refrigerator – it's achieved by pumping heat out of the back. To chill our beer we warm the world).

'Sheer' simplicity no. 1 – minimized time complexity, minimized state space complexity – zero referential mutation. Purely declarative – none of the complexities of a little imaginary homunculus running back and forth 'doing' things inside the machine at run-time.

(() => {
    
    // longestClipLine :: () -> String
    const longestClipLine = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        }).theClipboard()
        .split(/[\n\r]/)
        .reduce(
            (a, x) => {
                const lng = x.length;
                return lng > a[0] ? (
                    [lng, x]
                ) : a;
            }, [0, '']
        )[1];

    return longestClipLine();
})();

but that comes at various prices, including the use of 3 or 4 different idioms (sub-languages) in the same piece of code: Regexes, object methods, array indices, functions. It's like a conversation in a Johannesburg lift.

We can prune back the idiomatic complexity, to express it all in terms of function calls.

'Sheer' simplicity No. 2

(() => {

    // longestClipLine :: () -> String
    const longestClipLine = () =>
        snd(foldl(
            (a, x) => {
                const lng = length(a);
                return lng > fst(a) ? (
                    Tuple(lng, x)
                ) : a;
            }, Tuple(0, ''),
            lines(clipboard())
        ));

    // JXA ------------------------------------------------

    // clipboard :: () -> a
    const clipboard = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        }).theClipboard();

    // GENERICS -------------------------------------------

    // foldl :: (a -> b -> a) -> a -> [b] -> a
    const foldl = (f, a, xs) => xs.reduce(f, a);

    // fst :: (a, b) -> a
    const fst = tpl => tpl[0];

    // length :: [a] -> Int
    const length = xs => xs.length;

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // snd :: (a, b) -> b
    const snd = tpl => tpl[1];

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = (a, b) => ({
        type: 'Tuple',
        '0': a,
        '1': b,
        length: 2
    });

    // MAIN ---
    return longestClipLine();
})();

(we have now displaced some complexity out of the new code into the import list – less code-switching for the reader, and less fresh code required for the writer, but we are still pumping heat out of the back of the fridge. We now need more imports).

And if we're going to improve simplicity of reading and writing by importing library code, then we might as well make fuller use of the library.

'Sheer' simplicity No. 3

(() => {

    // longestClipLine :: () -> String
    const longestClipLine = () =>
        maximumBy(
            comparing(length),
            lines(clipboard())
        );


    // JXA ------------------------------------------------

    // clipboard :: () -> a
    const clipboard = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        }).theClipboard();

    // GENERICS -------------------------------------------

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        (x, y) => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

    // length :: [a] -> Int
    const length = xs => xs.length;

    // maximumBy :: (a -> a -> Ordering) -> [a] -> a
    const maximumBy = (f, xs) =>
        0 < xs.length ? (
            xs.slice(1)
            .reduce((a, x) => 0 < f(x, a) ? x : a, xs[0])
        ) : undefined;

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // MAIN ---
    return longestClipLine();
})();

The beer is well chilled now, no moving parts, and very high code reuse – we have pumped a lot of the local heat out into the imports.

But there is still a fundamental kind of complexity that all of these versions, (including the various applescripts above) have been sweeping under the carpet. It will soon trip users up, and if this code is embedded in anything larger, it may take the car off the road entirely. Nothing could be more complex.

If you think of all of these snippets as functions from a context to a result, then they are only partial functions. They are undefined (and will break) if there is no text in the clipboard ...

So, in case we've just copied a shape in OmniGraffle, rather than a document in TaskPaper, Sublime, or BBEdit

'Sheer' simplicity No. 4

(() => {

    // longestClipLineLR :: () -> Either String String
    const longestClipLineLR = () => {
        const clip = clipboard();
        return bindLR(
            'string' !== typeof clip ? (
                Left('No text in clipboard')
            ) : Right(clip),
            txt => Right(
                maximumBy(
                    comparing(length),
                    lines(txt)
                )
            )
        );
    };

    // JXA ------------------------------------------------

    // clipboard :: () -> a
    const clipboard = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        }).theClipboard();

    // GENERICS -------------------------------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        (x, y) => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

    // length :: [a] -> Int
    const length = xs => xs.length;

    // maximumBy :: (a -> a -> Ordering) -> [a] -> a
    const maximumBy = (f, xs) =>
        0 < xs.length ? (
            xs.slice(1)
            .reduce((a, x) => 0 < f(x, a) ? x : a, xs[0])
        ) : undefined;

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // MAIN ---
    return longestClipLineLR();
})();

Now the value returned is of a more complex type (a record with either a Right value (longest string) or a Left value (Message reporting a text-free clipboard), but including this code inside other code just got simpler.

There's no one 'sheer' simplicity. It all depends on who you are and what you are familiar with – what you want to chill, and where you are happy to divert the heat : -)

1 Like

Several different definitions of 'complexity' at work here, of course. The relevance of each is just a function of the practical context.

  • In terms of formal time complexity (Big 0), the complexity of your draft there is increased by the repeated remeasuring of the length of the currently longest string.
  • In terms of space-state complexity, any use of an imperative mode is massively more complex than use of a declarative or functional mode, because the references of names like theLine, longestLine are going through a whole series of mutations.
  • In terms of operational complexity and code reuse, naked code is more complex to reuse than a wrapped function
  • In terms of single lines of new code required (sloc), 8 new lines is more complex than 1 new line

etc etc etc . Real 'simplicity' entirely depends on which part you need to simplify, and why, and for who.

In my context I usually need cognitive simplicity, and simplicity of reuse.

Just writing

maximumBy(comparing(|length|), paragraphs of the clipboard)

(and importing the rest) has one kind of simplicity,

8 sloc of fresh unwrapped imperative code ('first I set this, then I repeatedly do that, finally I return the other' etc, has another ...

'Simplicity' is more complex than it looks : - )

( and not always easy to disentangle from familiarity, which is often what people really mean by it )

3 Likes