2

I'm having a hard time finding examples (videos or blogs) of functional programming object construction patterns.

I recently encountered the below snipped builder pattern and I like the experience it provides for constructing an object with nesting. When it's a flatter object, I'd normally just use a simple object factory with an opts param to spread with defaults, but passing in a nested array of objects starts to feel messy.

Are there FP patterns that can help make composing an object with nesting like the below comfortable while allowing for calling some methods n times, such as addStringOption?

const data = new SlashCommandBuilder()
    .setName('echo')
    .setDescription('Replies with your input!')
    .addStringOption(option =>
        option.setName('input')
            .setDescription('The input to echo back')
            .setRequired(true)
    )
    .addStringOption(option =>
        option.setName('category')
            .setDescription('The gif category')
            .setRequired(true)
            .addChoices(
                { name: 'Funny', value: 'gif_funny' },
                { name: 'Meme', value: 'gif_meme' },
                { name: 'Movie', value: 'gif_movie' },
  ));

data ends up looking something like:

{
  name: "echo",
  description: "Replies with your input!",
  options: [
    {
      name: "input",
      description: "The input to echo back",
      type: 7, // string option id
      required: true,
      choices: null,
    },
    {
      name: "category",
      description: "The gif category",
      required: true,
      type: 7,
      choices: [
        { name: "Funny", value: "gif_funny" },
        { name: "Meme", value: "gif_meme" },
        { name: "Movie", value: "gif_movie" },
      ],
    },
  ],
};

Below is what I'm playing around with. I'm still working on learning how to type them in TS so I'm sharing the JS.

Allowing for method chaining in the below snippet is maybe contorting FP too much make it like OOP, but I haven't found an alternative that makes construction flow nicely.

An alternative could be standalone builders each returning a callback that returns the updated state then pipe these builders together, but with some builders being callable n times it's hard to make and provide the pipe ahead of time and without the dot notation providing intellisense it seems harder to know what the available functions are to build with.

const buildCommand = () => {
  // data separate from methods.
  let command = {
    permissions: ['admin'],
    foo: 'bar',
    options: [],
  };

  const builders = {
    setGeneralCommandInfo: ({ name, description }) => {
      command = { ...command, name, description };
      // trying to avoid `this`
      return builders;
    },

    setCommandPermissions: (...permissions) => {
      command = { ...command, permissions };
      return builders;
    },

    addStringOption: (callback) => {
      const stringOption = callback(buildStringOption());
      command = { ...command, options: [...command.options, stringOption] };
      return builders;
    },
    // can validate here
    build: () => command,
  };

  return builders;
};

const buildStringOption = () => {
  let stringOption = {
    choices: null,
    type: 7,
  };

  const builders = {
    setName: (name) => {
      stringOption = { ...stringOption, name };
      return builders;
    },

    setDescription: (description) => {
      stringOption = { ...stringOption, description };
      return builders;
    },

    addChoices: (choices) => {
      stringOption = { ...stringOption, choices };
      return builders;
    },

    build: () => stringOption,
  };

  return builders;
};

const command1 = buildCommand()
  .setGeneralCommandInfo({ name: 'n1', description: 'd1' })
  .setCommandPermissions('auditor', 'moderator')
  .addStringOption((option) =>
    option.setName('foo').setDescription('bar').build()
  )
  .addStringOption((option) =>
    option
      .setName('baz')
      .setDescription('bax')
      .addChoices([
        { name: 'Funny', value: 'gif_funny' },
        { name: 'Meme', value: 'gif_meme' },
      ])
      .build()
  )
  .build();

console.log(command1);
2
  • For a start I wouldn't use such deeply nested objects. Rather I see three or more classes with an outer class having fields to be set with objects from the other two classes. This breaks it down into chunks and in particular gives the nested chunks hopefully meaningful names. Building the chunks may require no more than a constructor or a really simple builder if you wish. Commented Aug 25, 2022 at 17:33
  • @Harald that is how the example builder works, but in FP I thought classes that mix methods and data were generally avoided so I was trying not to use classes. Commented Aug 25, 2022 at 23:22

1 Answer 1

3

Why not simply create and use data constructors?

const SlashCommand = (name, description, options) =>
  ({ name, description, options });

const StringOption = (name, description, required, type = 7, choices = null) =>
  ({ name, description, required, type, choices });

const Choice = (name, value) => ({ name, value });

const data = SlashCommand('echo', 'Replies with your input!', [
  StringOption('input', 'The input to echo back', true),
  StringOption('category', 'The gif category', true, undefined, [
    Choice('Funny', 'gif_funny'),
    Choice('Meme', 'gif_meme'),
    Choice('Movie', 'gif_movie')
  ])
]);

console.log(data);

TypeScript Playground Example

You can't get more functional than this. Intellisense will also help you with the constructor arguments.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.