Insult Generator

So, a while back, I asked my spouse to give me a programming assignment. One of the assignments was to make an insult generator.

Well, now I've completed it! :)

The source code is here:
https://github.com/NerdcoreSteve/insult

And the app is here:
https://insult-you.herokuapp.com/

If you click on the "new insult" button you'll get a randomly generated insult. Here are some choice ones:

"Your nose associates with fuzzy bunnies!"

"Your face is a hamster!"

"Your mother smells like beehives!"

Strong words, I know. I'm sorry if I've hurt your feelings. But cheer up. It's only an app after all.

Here's the page's main JavaScript:

var insult = require('./insult')  
var rand = require('./rand.js')  
var insult_data = require('./insult_data')

//Impure app code
//TODO keyboard bindings
document.querySelector('.insult-button').onclick = () =>  
    document.querySelector('.insult').innerHTML =
        insult(
            insult_data,
            rand(0, insult_data.subjects.length - 1),
            rand(0, insult_data.verbs.length - 1),
            rand(0, insult_data.objects.length - 1))

I've not talked about it yet, but document.querySelector is a built-in browser function that grabs a piece of your web page (represented in browser memory as a bunch of nested objects called the DOM, or document object model) and allows you to do stuff with or to it.

Here I grab the button by its class (.insult-button) and add an onclick method to it. When the button is clicked the method will fire.

The onclick method uses document.querySelector again to grab another part of the page where the insult is displayed. It's then given the return value of insult, which is a string, and places the insult in the page using the innerHTML property.

insult takes 4 parameters: insult_data (an object I'll talk about in a bit) and 3 randomly generated numbers. Each of those numbers is an index used to select from a list of subjects, verbs, and objects respectively.

Note that this code is not purely functional. There is a side effect, namely the modification of the page. And rand does not return the same value every time you give it the same parameters.

insult, however, is a pure function.

But before I go into insult, here's insult_data.js:

module.exports = {  
    subjects: [
        {
            part: 'You',
            first_person: true
        },
        {
            part: 'Your mother',
            first_person: false
        },
        {
            part: 'Your father',
            first_person: false
        },
        {
            part: 'Your face',
            first_person: false
        },
        {
            part: 'Your nose',
            first_person: false
        }
    ],
    verbs: [
        {
            first_person: 'smell like',
            third_person: 'smells like',
            plural: true
        },
        {
            first_person: 'are',
            third_person: 'is',
            plural: false
        },
        {
            first_person: 'associate with',
            third_person: 'associates with',
            plural: true
        }
    ],
    objects: [
        {
            singular: 'a hamster',
            plural: 'hamsters'
        },
        {
            singular: 'a fuzzy bunny',
            plural: 'fuzzy bunnies'
        },
        {
            singular: 'an elderberry',
            plural: 'elderberries'
        },
        {
            singular: 'a beehive',
            plural: 'beehives'
        }
    ]
}

The idea is that we take this object and reduce it down to an object that we'll use to construct our sentence for us.

Here's an example of what that object might look like:

{ subject: 'Your mother',
  first_person: false,
  verb: 'associates with',
  plural: true,
  object: 'hamsters' }

Here's insult.js:

var insult = (insult_data, subject_index, verb_index, object_index) =>  
    R.compose(
        insult_object =>
            `${insult_object.subject} ${insult_object.verb} ${insult_object.object}!`,
        object(object_index),
        verb(verb_index),
        subject(subject_index))
            (insult_data)

It's a simple composition chain. First we get the subject, then the verb, then the object (remember that with compose functions are called last-function-first).

After our insult object is constructed we feed it into a template. I know I've not yet talked about ES2015 string templates but I think this should be clear:

`${insult_object.subject} ${insult_object.verb} ${insult_object.object}!`

We're simply constructing a sentence from our sentence parts: subject, verb, and object.

Here's what subject looks like:

module.exports = R.curry((subject_index, insult_data) =>  
    R.compose(
        R.omit(['subjects']),
        R.over(
            first_person_lens,
            R.compose(
                R.prop('first_person'),
                R.nth(subject_index))),
        R.over(
            subject_lens,
            R.compose(
                R.prop('part'),
                R.nth(subject_index))))
                    (insult_data))

subject_lens's source and destination are subjects and subject respectively. This lets us take the value from subjects and add a value for subject.

(Note, that by "add a value" what I really mean is create a new intermediary object that differs from the original object, thus keeping our function pure).

The first R.over call gets the subject with the index of subject_index, grabs its part property and places the part value in insult_data's subject property.

first_person_lens's source and destination are subjects and first_person. This lets us use R.over again to grab the appropriate first_person property, which will be used by verb.

I talk about lenses here.

Finally, we get rid of subjects with R.omit. Not necessary but it cleans up the object we hand to verb.

Here's verb:

module.exports = R.curry((verb_index, insult_data) =>  
    R.compose(
        R.omit(['verbs']),
        R.assoc('plural', insult_data.verbs[verb_index].plural),
        R.over(
            verb_lens,
            R.compose(
                R.ifElse(
                    () => insult_data.first_person,
                    R.prop('first_person'),
                    R.prop('third_person')),
                R.nth(verb_index))))
                    (insult_data))

The first R.over call grabs the verb we're going to use (chosen by the given index) and (depending on the value of insult_data.first_person) puts that verb's first_person or third_person value in insult_data's verb property.

The idea here is that if the subject is "You" then we want to select the appropriate verb. "are" not "is". "smell" not "smells".

The same thing happens with the verb-object handoff. It passes an object with a plural value to make sure the object is "an elderberry" or "elderberries" as appropriate.

Another thing I wanted to point out was the testing I did. I'm no testing expert, and I wonder if I've done a bit of overkill, but I've tested damn near everything.

Here's a sample of some of the tests I've written.

insult_data.js:

    it('has the property "verbs" which has an array as its value',
        () => expect(
            Array.isArray(
                insult_data.verbs))
                    .toEqual(true))

insult.js:

    it('should return a string',
        () => expect(
            typeof insult(
                insult_data, 1, 2, 0))
                    .toEqual('string'))

verb.js:

    it('should add the verb part from the given index (third_person when first_person is false)',
        () => expect(
            verb(
                1, 
                insult_data_third_person).verb)
                    .toEqual('is'))

I don't know if I've covered everything I ought, but I wrote 26 tests in all. I didn't check for certain, but I'm pretty sure the code for the tests has more lines than the app itself.

What's weird is that I can think of all kinds of improvements and additions to this app. I've made a public trello board here. I intend to make as many improvements as I can.

I've got so many ideas for this little app that it's going to be a while before I need another assignment!

Looking for a software developer?