What’s more fantastic than fantasy land? An Introduction to Static land

栏目: IT技术 · 发布时间: 4年前

内容简介:Fantasy land is great. It provides a standard naming convention for these things calledalgebraic structures. It allows a single function to work with a plethora of structures. No modification required. And it gets better. We don’t even have to write the fu

Fantasy land is great. It provides a standard naming convention for these things calledalgebraic structures. It allows a single function to work with a plethora of structures. No modification required. And it gets better. We don’t even have to write the functions. Libraries like Ramda are already compliant. So we have this whole world of interoperable functions and structures open to us. The title ‘fantasy land,’ though originally a joke, is quite fitting.

Trouble in Fantasy Land

Fantasy land isn’t perfect though. And it’s not the only way to do algebraic structures in JavaScript. Some of the trouble with fantasy land comes from its implementation. It assumes that we use objects and methods for everything. And that’s a totally reasonable way to do things. But it’s not the only way. And it has some drawbacks.

Name conflicts and namespacing

One of the drawbacks is name conflicts. Early versions of fantasy land had straightforward method names. That is, names like:

equals
concat
empty
map
of
reduce
sequence
chain
extend
extract

Many of the names were based on existing JavaScript interfaces, like Array methods. But, as Scott Sauyet put it , the trouble is “these are very common English words, with many meanings.” So it’s easy to run into problems if you’re working in a domain that uses those names in a different context. For example, you might be creating a geospatial application. In that context, map might have a different meaning. That might seem like a trivial example, but it comes up more often than anyone would like.

To avoid this, the fantasy land authors agreed to namespace all the method names. So now, instead of calling x.map(f) , we now call x['fantasy-land/map'](f) . It solves the conflict problem. But it’s not pretty. It makes the specification hard to read. And it makes the methods inconvenient to type manually. All in all, it’s not much fun .

Now, this isn’t quite as bad as it sounds. That is, it’s not so bad if you understand the intent of Fantasy Land. You see, Fantasy Land isn’t really intended for us mere mortals. Instead, it’s intended for use by library authors . The idea being, us mortal programmers shouldn’t need to type these method names by hand. The expectation is that we’d be using a library like Ramda. So instead of something like this:

import Maybe from 'my/maybe/library/somewhere';

const noStupid = s => (s.includes('stupid')) ? Maybe.Just(s) : Maybe.Nothing;

// These namespaced method calls look silly.
const title         = new Maybe('Yes, this is a silly example');
const sentence      = title['fantasy-land/map'](s => `${s}.`);
const validSentence = sentence['fantasy-land/chain'](noStupid);

With Ramda, we would pull in functions like map() , chain() and pipe() to manipulate our structures:

import Maybe from 'my/maybe/library/somewhere';
import {chain, map, pipe} from 'ramda';

const noStupid = s => (s.includes('stupid')) ? Maybe.Just(s) : Maybe.Nothing;

// Note the lack of method calls in our pipe(). Much prettier.
// But, we did have to pull in the whole Ramda library to make
// it happen.
const title = new Maybe('Yes, this is a silly example');
const validSentence = pipe(
    map(s => `${s}.`),
    chain(noStupid),
)(title);

As you can see, once we introduce Ramda, all the Fantasy Land prefixes disappear. So namespaces aren’t so bad, right? We don’t have to worry about them anymore. Ramda just takes care of it. Everyone’s happy, yes?

Except, those prefixes aren’t gone. They’re just hidden. And they keep poking their little heads out. For example, consider Maybe.of() . With the namespace prefix it becomes Maybe['fantasy-land/of'] . It’s a static method. So there’s no Ramda function for that. This means that if we want to use that static method, we’re stuck writing out the prefix. That, or we write our own alias for it. And that’s OK. But not much fun.

None of this is the end of the world. It’s just inconvenient. It’s friction. And it would be nice if there was less friction.

Wrapping and unwrapping values

The other drawback Fantasy Land has is all the wrapping and unwrapping. To make things work with Fantasy Land were forever wrapping up values inside objects. And sometimes, it’s objects inside objects, inside objects. And that’s not much fun either. Most of the time, it’s all fine. But at some point, we need to work with something outside our world of algebraic structures. Perhaps a DOM element or React component. Or even a database connection. Here, we have two options:

  1. Unwrap values out of our algebraic structures somehow, or
  2. Wrap the outside thing into a fantasy-land structure.

Either way, we’re either wrapping or unwrapping somewhere.

This wrapping business is actually a good thing. Especially if you’re a beginner at functional programming. The wrapping and unwrapping forces you to think about types. That’s important in a loosey-goosey-typedlanguage like JavaScript. For example, consider a simple implementation ofMaybe. We can’t just concatenate a Maybe onto the end of a String.

import Maybe from 'my/maybe/library/somewhere';
const valueIGotFromParsingJSON = new Maybe('Another silly example');
const sentencifiedTitle = valueIGotFromParsingJSON + '.'; // This doesn't work.

If we want to get the value out of the Maybe container, we have to use something like .orElse() .

import Maybe from 'my/maybe/library/somewhere';
const valueIGotFromParsingJSON = new Maybe('Another silly example');
const sentencifiedTitle = valueIGotFromParsingJSON.orElse('No title found') + '.';

Again, this is a good thing. It forces us to consider what happens if the value is null . And that’s the whole point of Maybe. We can’t fool ourselves into thinking that null isn’t a possibility. Similarly, Task forces us to think about what happens if an operation fails. And Either can force us to think about how we’re going to deal with exceptions.All good things.

Still, wrapping and unwrapping creates drag. Once you’re more experienced, those objects can start to feel a little heavy. A good library like Ramda helps. And, as we saw earlier, once you have some good pipelines set up the containers start to disappear. But it’s still a drag. It’s not terrible. Just inconvenient. Particularly when wrapping things that are already objects, like DOM elements or Promises. They have their own set of methods. But to get at them you have to go via .map() , .ap() or .chain() . Not hard. Just a drag.

An alternative

So, Fantasy Land isn’t perfect. In fact, it can be a bit annoying at times. And some of that is JavaScript’s fault. But not all of it. Still, imagine if we could have algebraic structures without those drawbacks. What if there was a way to create structures without having to worry so much about name conflicts? And imagine if we didn’t have to wrap all our data in objects. We could work with strings, numbers or even DOM elements, just as they are. No wrapping or unwrapping. Algebraic structures with plain ol’ JS data types.

Sound a bit fantastical? Well it’s real. It’s made possible by the Static Land specification.

What’s more fantastic than fantasy land? An Introduction to Static land

What’s Static Land then? Well, like Fantasy Land, Static Land is a specification for common algebraic structures. Fantasy Land assumes that you’re creating structures using objects and methods. But Static Land assumes you’re creating structures using plain ol' JavaScript functions. But they must be static functions. That means that we can’t use the magic this keyword anywhere. We’re still free to have classes, objects and modules. We can group our functions together as we like. But the functions themselves can’t be methods . No this .

Now, if you’ve had some training in computer science, that might sound regressive. Especially if you work with languages like C# or Java. In my university classes they taught us to move beyond those quaint static modules of the past. They taught us to embrace Object-Oriented Programming (OOP). The way of the future! So I spent a lot of time developing intuitions about classes and objects. That was the Best Practice™️ way to build programs. But, functional programming throws many of my old intuitions on their head. And Static Land gets the job done entirely with static methods. It’s great.

An example

What does a Static Land algebraic structure look like? Perhaps the best way to show this is by example. We’ll use static-land versions of Maybe and List (arrays), but we’ll do it using a real life kind of problem. A problem thousands of web developers are working on right this second. The problem is this: We have some settings data we got from a server somewhere. We want to put those values into a form on some kind of settings screen. That is, we’re making an HTML form.

Sticking values in HTML form fields. I’d estimate it’s a large chunk of what most of us professional web developers do all day. Let’s look how a static land version of Maybe and List can help get it done.

In our imaginary problem, we have not one, but two blobs of data. Perhaps we fetched them via an XHRequest. Perhaps we read them from a file. It doesn’t matter. The point is, we have two of them:

  1. One blob of data to specify the form structure; and
  2. One blob of data that has the values for the form.

We want to take these two blobs, smush them together, and create some HTML representing our form. Here’s some sample data to show what I’m talking about. First, the form specification:

const formSpec = [
    {
        id: 'person-name',
        label: 'Name',
        type: 'text',
        name: 'personname',
        dflt: '',
    },
    {
        id: 'person-email',
        label: 'Email',
        type: 'email',
        name: 'personemail',
        dflt: '',
    },
    {
        id: 'wonderland-resident',
        label: 'Are you a resident of Wonderland?',
        type: 'checkbox',
        name: 'resident',
        options: [
            {
                label: 'Yes, I am a resident',
                value: 'isresident',
            },
        ],
    },
    {
        id: 'comments',
        label: 'Comments',
        type: 'textarea',
        dflt: '',
        name: 'comments',
    },
    {
        id: 'submitbtn',
        label: 'Submit',
        type: 'submit',
    },
];

And second, the form data:

const formValues = [
    {
        id: 'person-name',
        value: 'Cheshire Cat',
    },
    {
        id: 'person-email',
        value: 'cheshire.cat@example.com',
    },
    {
        id: 'wonderland-resident',
        value: ['isresident'],
    },
];

With these two data structures, we have enough information here to create some kind of form.

List

We’ve got a motivating example now. Let’s take a look at what a Static Land structure might look like. Here’s an implementation of List. It’s not the only way to implement List. And perhaps it’s not the best way to implement list. But it will do for now.

// Curry function stolen from Professor Frisby's Mostly Adequate Guide
// curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
function curry(fn) {
    const arity = fn.length;
    return function $curry(...args) {
        if (args.length < arity) {
            return $curry.bind(null, ...args);
        }

        return fn.call(null, ...args);
    };
}

// Unary takes a function and makes it ignore everything
// except the first argument.
// unary :: ((a, b, ...) -> c) -> a -> c
function unary(f) {
    return x => f(x);
}

// The List implementation itself.
const List = {

    // map :: (a -> b) -> List a -> List b
    map: curry(function map(f, xs) {
        return xs.map(unary(f));
    }),

    // chain :: (a -> List b) -> List a -> List b
    chain: curry(function chain(f, xs) {
        return xs.flatMap(unary(f));
    }),

    // ap :: List (a -> b) -> List a -> List b
    ap: curry(function ap(fs, xs) {
        return List.chain(f => List.map(f, xs), fs);
    }),

    // reduce :: (a -> b -> a) -> a -> List b -> a
    reduce: curry(function reduce(f, a, xs) {
        return xs.reduce(f, a);
    }),
};

It doesn’t look like much, does it? We’re mostly just delegating to built-in methods. Even with unary() and curry() making things more verbose, it’s still not long.

The unary() function is there as a guard. It makes sure that callback functions only see a single parameter. This can be handy when using a function like parseInt() . Functions that take an optional second (or third) parameter can cause problems. The built-in .map() passes three parameters to the callback function:

  1. The value from the array;
  2. The current index; and
  3. the entire array itself.

Now parseInt() , for example, will interpret the index as the radix (also known as base). That’s not usually what we want. So we use unary() to prevent confusion.

Back to our example though. How do we use List?

We’ll start by defining a few utility functions. For simplicity, these return strings. It wouldn’t be hard to change them to return, say, React components though. For now, we’ll leave them as strings.

function sanitise(str) {
    const replacements = [
        [/</g, '<'],
        [/"/g, '"'],
        [/'/g, '''],
        [/\\/g, ' '],
    ];
    const reducer = (s, [from, to]) => s.replace(from, to);
    return List.reduce(reducer, String(str), replacements);
}

function text({id, label, dflt, value, name}) {
    return `
    <div>
      <label for="${id}">${label}</label>
      <input type="text" name="${name}" value="${sanitise(value)}" id="${id}" />
    </div>`;
}

function email({id, label, dflt, value, name}) {
    return `
    <div>
      <label for="${id}">${label}</label>
      <input type="email" name="${name}" value="${sanitise(
        value,
    )}" id="${id}" />
    </div>`;
}

function checkboxItem(value) {
    return ({label: lbl, value: val, name}) =>
        `<li><input type="checkbox" name="${name}" checked="${
            val === value ? 'checked' : ''
        }" value="${sanitise(val)}" /><label for="">${lbl}</label></li>`;
}

function checkbox({id, label, type, options, value, name}) {
    return `
    <fieldset id="${id}">
      <legend>${label}</legend>
      <ul>
      ${List.map(checkboxItem(value), options).join('')}
      </ul>
    </fieldset>`;
}

function textarea({id, label, value, dflt, name}) {
    return `
    <div>
      <label for="${id}">${label}</label>
      <textarea name="${name}" id="${id}">${sanitise(value)}</textarea>
    </div>`;
}

There’s nothing particularly interesting going on here. A little destructuring; a little string interpolation. No big deal. We’ve already used List.map() and List.reduce() . Note how we casually call .join() straight after calling List.map() in checkbox() . That’s a native array method right there. No unwrapping. No proxy methods. Just a straight value. Neat, huh?

Two minor bits of cleverness to note in these utility functions:

  1. The destructured parameter names look a lot like the keys in our form structure data blob. (That is, our formSpec variable).
  2. The names of our HTML functions match up rather well with the values for type in our form structure. (That’s formSpec again).

These are deliberate choices. We’ll see how they help in a little bit. (If you haven’t figured it out already).

Getting back to the data, we have two blobs: formSpec and formData . The first, formSpec , has almost everything we need. But it’s missing some data. We need those values from formData . And we need some way to smush those two data structures together. As we go, we also need to make sure the right values end up in the correct form fields.

How do we know which form values go with which specification? By matching the id fields in each object. In other words, we want to match each entry in formData with an entry in formSpec . And then smush those two objects together. We should end up with a new array of smushed objects that have the pre-filled values we want.

Let’s put that another way. For each item in formSpec , we want to check to see if there’s an item in formData with the same id . If so, then we want to merge those values together. It might look something like this:

const mergeOnId = curry(function mergeOnId(xs, ys) {
    return List.map(
        x => Object.assign(x, ys.find(y => x.id === y.id)),
        xs,
    );
});

This function takes the first list, and runs through each item. For each item it looks for a corresponding item in the second list. If it finds one, it merges the two. If it doesn’t find one, it merges undefined , which returns the same object. It may not be the most efficient way of doing it, but it gets the job done.

Something bothers me about this function though. It’s a little too specific. We’ve hard-coded the field we’re matching on, id . It might give us some more flexibility if we made that field a parameter. So let’s rewrite our function to do that:

const mergeOn = curry(function mergeOn(key, xs, ys) {
    return List.map(
        x => Object.assign(x, ys.find(y => x[key] === y[key])),
        xs,
    );
});

We have a way to merge our big list of form data. Next, we want to turn that form data into HTML. We do that by creating a function that looks at a given entry and calls the appropriate utility function. It might look something like this:

function toField(data) {
    const funcMap = {text, email, checkbox, textarea};
    return funcMap[data.type](data);
}

So, we could (if we wanted to) run toField() with List.map() to get an array full of HTML strings. But we don’t really want an array, we want one big string of HTML. We want to go from lots of values in the list down to a single value. Sounds like a job for List.reduce() .

function formDataToHTML(formData) {
    return List.reduce(
        (html, fieldData) => html + '\n' + toField(fieldData),
        '',
        formData
    );
}

And from there it’s not too difficult to compose everything together…

// Pipe stolen from “JavaScript Allongé, the "Six" Edition,”
// by Reg “raganwald” Braithwaite.
// Pipe composes functions in reverse order.
function pipe(...fns) {
    return value => fns.reduce((acc, fn) => fn(acc), value);
}

const wrapWith = curry(function wrapWith(tag, data) {
    return `<${tag}>${data}</${tag}>`;
});

function processForm(formSpec, formValues) {
    return pipe(
        mergeOn('id', formSpec),
        formDataToHTML,
        wrapWith('form'),
    )(formValues);
}

You can see the whole thing working together in this code sandbox .

We have a neat little implementation. But perhaps it’s somewhat… underwhelming. We haven’t used any List functions besides map() and reduce() . It doesn’t seem worth introducing List for two functions. And they’re built-ins anyway. But my goal here isn’t to show you the absolute best way to build an HTML form. Rather, it’s to show how working with Static Land might look in practice.

To that end, let’s introduce Maybe as well. That way we can see two algebraic structures working together.

Maybe

There’s some problems with our code so far. First, notice that when we run our code, the comment area shows ‘undefined’. That’s less than ideal. One way to deal with this is to add some default values to our form specification. The new specification might look like so:

const formSpec = [
    {
        id: 'person-name',
        label: 'Name',
        type: 'text',
        name: 'personname',
        dflt: '',
    },
    {
        id: 'person-email',
        label: 'Email',
        type: 'email',
        name: 'personemail',
        dflt: '',
    },
    {
        id: 'wonderland-resident',
        label: 'Are you a resident of Wonderland?',
        type: 'checkbox',
        name: 'resident',
        options: [
            {
                label: 'Yes, I am a resident',
                value: 'isresident',
            },
        ],
        dflt: '',
    },
    {
        id: 'comments',
        label: 'Comments',
        type: 'textarea',
        dflt: '',
        name: 'comments',
    },
];

All we’ve done is add some default values using the key dflt .So, we’ll continue to merge the two data structures as before. But we need some way to merge the dflt values with the value values. That is, if there is no value then use dflt . Sounds like a job for Maybe.

So, a simple Maybe implementation might look like this:

const isNil = x => (x === null || x === void 0);

const Maybe = {
    // of :: a -> Maybe a
    of: x => x,
    
    // map :: (a -> b) -> Maybe a -> Maybe b
    map: curry(function map(f, mx) {
        return isNil(mx) ? null : f(mx);
    }),
    
    // ap :: Maybe (a -> b) -> Maybe a -> Maybe b
    ap: curry(function ap(mx, mf) {
        return isNil(mf) ? null : Maybe.map(mf, mx);
    }),
    
    // chain :: (a -> Maybe b) -> Maybe a -> Maybe b
    chain: curry(function chain(f, mx) {
        return Maybe.map(f, mx);
    }),
    
    // orElse :: a -> Maybe a -> a
    orElse: curry(function orElse(dflt, mx) {
        return isNil(mx) ? dflt : mx;
    }),
}

It’s a little bit different if you’re used to the Fantasy Land way of doing things. Our .of() function is just identity. And chain() just calls map() . But it’s still a valid implementation of Maybe. It encapsulates all those isNil() checks for us. So how might we use it?

Let’s start by setting those default values. We’ll create a new function for the purpose:

function setDefault(formData) {
    return {
        ...formData,
        value: Maybe.orElse(formData.dflt, formData.value),
    };
}

We can compose this function with toField() when we process each item. So our formDataToHTML() function becomes:

function formDataToHTML(formData) {
    return List.reduce(
        (html, fieldData) => html + '\n' + toField(setDefault(fieldData)),
        '',
        formData
    );
}

There’s a second problem with our code though. This time it’s in the toField() function. And it’s potentially more serious than printing ‘undefined’ in a text field. Let’s take a look at the code for toField() again:

function toField(data) {
    const funcMap = {text, email, checkbox, textarea};
    return funcMap[data.type](data);
}

What happens if our form specification changes and introduces a new type of field? It will try to call funcMap[data.type] as a function. But there is no function. We’ll get the dreaded “undefined is not a function” error. That’s never fun. Fortunately, Maybe can help us out. We have a function that may be there, or it may be undefined. From a static-land point of view, this is already a Maybe. So, we can use Maybe.ap() to apply the function to a value.

function toField(data) {
    const funcMap = {text, email, checkbox, textarea};
    return Maybe.ap(funcMap[data.type], data);
}

And suddenly, the problem just disappears. It’s like magic.

Here’s what it looks like when we compose it together:

// Pipe stolen from “JavaScript Allongé, the "Six" Edition,”
// by Reg “raganwald” Braithwaite.
// Pipe composes functions in reverse order.
const pipe = (...fns) => (value) =>
    fns.reduce((acc, fn) => fn(acc), value);

const wrapWith = curry(function wrapWith(tag, data) {
    return `<${tag}>${data}</${tag}>`;
});

function processForm(formSpec, formValues) {
    return pipe(
        mergeOn('id', formSpec),
        List.map(setDefault),
        formDataToHTML,
        wrapWith('form'),
    )(formValues);
}

See the whole thing working together in this Code Sandbox .

Weighing up the pros and cons

Now, you may find all this a little… dull; unimpressive; ho hum, even. In fact, I’m hoping you do. That’s kind of the point. Static Land algebraic structures aren’t any more complicated than Fantasy Land ones. They just come at the problem in a different way. They have a different set of design trade-offs.

Those design trade-offs are worth thinking about. We lose some type safety implementing Maybe this way.We’re no longer forced to use something like .orElse() to extract a value. We might get a little lax if we’re not careful. But at the same time, you can see how nice this is. We can use algebraic structures without wrapping and unwrapping values all the time. To me, it feels more natural. That’s completely subjective, I know, but that doesn’t make it irrelevant.

Another trade-off is that we lose the ability to use utility libraries like Ramda in the same way. With Fantasy Land, we can write a map() function that delegates to myObject['fantasy-land/map']() . And map() will then work with any object that has a fantasy-land/map method. In the examples above, however, we had to be explicit about which map() function we were calling. It was either List.map() or Maybe.map() . So, we’re doing some work that a compiler might otherwise do for us. Furthermore, writing out all those prefixes (i.e. List or Maybe ) gets annoying.

Finally, there’s something else to consider with regards to wrapping and unwrapping. Notice how we were able to use List with plain ol' JavaScript arrays. We didn’t have to call myList.__value.find() to make our merge function. It makes our code easier to integrate. We’re not using a custom-made class. It’s native JavaScript data types and functions. That’s it.

But which one is better?

So, you might be wondering: “Which one is better?” And you probably know what I’m going to say: “It depends”. Static land is a mixed bag. We gain some convenience and interoperability, but at a cost. We end up writing out a bunch of module prefixes. We swap one namespace work-around for another. So they come out roughly even.

That said, in certain situations, Static Land really shines. For example, you may be working with React components or DOM elements. And asking the rest of your team to wrap them up in another layer may be too much. It’s not worth the effort to make them work with Fantasy Land. But Static Land lets you work with those data types directly. Yet still maintain the benefits of algebraic structures. For those situations, it’s lovely.

But really, my main goal for this post was to raise some awareness for Static Land. Just to get it out there as an option. I don’t see many other people writing about it. But I think it’s cool and deserves more attention than it gets. So maybe take a look and see if it might come in handy for you.


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

高效程序的奥秘

高效程序的奥秘

沃瑞恩 / 冯速 / 机械工业出版社 / 2004-5 / 28.00元

本书适合程序库、编译器开发者及追求优美程序设计的人员阅读,适合用作计算机专业高年级学生及研究生的参考用书。  本书直观明了地讲述了计算机算术的更深层次的、更隐秘的技术,汇集了各种编辑的小技巧,包括常购的任务的小算法,2的幂边界和边界检测、位和字节的重排列、整数除法和常量除法、针对整数的基涵义,空间填充曲线、素数公式等。一起来看看 《高效程序的奥秘》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器