#Optional record field

1 messages · Page 1 of 1 (latest)

shadow wolf
#

I'd like to create a record type with optional fields, like:

pub type Command {
  Command(
    name: String,
    aliases: Option(List(String)),
    usage: Option(String),
    usage_text: Option(String),
    args_usage: Option(String),
    version: Option(String),
    description: Option(String),
    default_command: Option(String),
    category: Option(String),
    commands: Option(List(Command)),
    authors: Option(List(String)),
    copyright: Option(String),
    parent: Option(Command),
  )
}

This leads to a Gleam error on it having the wrong arity. How would you implement something like this in Gleam?

#

I can't imagine this is the ideal way of doing this

pub fn new(name: String) -> Command {
  {
    name: name,
    aliases: None,
    usage: None,
    usage_text: None,
    args_usage: None,
    version: None,
    description: None,
    default_command: None,
    category: None,
    commands: None,
    authors: None,
    copyright: None,
    parent: None,
  }
}

nor does a dict seem like the right choice

tacit trout
#

what exactly leads to the wrong arity error? are you calling like Command("foo")

shadow wolf
#

Sorry yeah, the idea behind the API would be you must call it with a name, everything else is optional. So Command("foo") or Command("foo", aliases: [ "bar" ]) would both be valid

#

I understand why this is an error, I guess I'm just wondering how a Gleam programmer should do this

#

would this be a case for dict.from_list?

tacit trout
#

yeah your example is pretty idomatic for a constructor

#

gleam has a strong "no optional parameters, no method overloading" policy

nocturne sail
shadow wolf
#

hmmm but this would need to be replicated for every combination of parameters passed in

#

what I'm thinking now is

pub type Fields {
  Name(value: String)
  Aliases(values: List(String))
  ...
}

and expose those somehow?

tacit trout
#

you can add the extra ones later like let command = Command(author: me, ..command)

#

as needed

shadow wolf
#

oh sure ok

tacit trout
#

if you want to get cute I think you can make a factory-style constructor thing with a custom type to enumerate each field

nocturne sail
#

Setter functions:

fn set_aliases(command: Command, aliases: Some(List(String))) -> Command {
  Command(..command, aliases:)
}

<make the rest of the args as setter functions>

let command =
  new_command_with_defaults("new!")
  |> set_aliases(Some(["new2"]))
  |> set_usage(Some("just do it"))
  ...
tacit trout
#
pub fn new_command(name) {
  Command(
    name,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
  )
}

pub fn main() {
  let wibble = Command(
    ..new_command("wibble"),
    authors: Some(["me", "you"]),
    copyright: Some("legal jargon"),
    version: Some("3.14.15"),
  )
}
shadow wolf
#

These are definitely good. I will say the API I'm trying to copy has like, dozens of fields so writing setters or None for each is not ideal. Neither is this, but it's another way to do it

pub type CommandArgument {
  Aliases(x: List(String))
  Usage(x: String)
  UsageText(x: String)
  ArgsUsage(String)
  Version(String)
  Description(String)
  DefaultCommand(String)
  Category(String)
  Commands(List(Command))
  Authors(List(String))
  Copyright(String)
  Parent(String)
}

pub opaque type Command {
  Command(name: String, arguments: Dict(String, CommandArgument))
}

pub fn new(name: String, arguments: List(CommandArgument)) -> Command {
  let args: Dict(String, CommandArgument) =
    arguments
    |> list.map(fn(argument) -> #(String, CommandArgument) {
      case argument {
        Aliases(argument) -> #("aliases", Aliases(argument))
        Usage(argument) -> #("usage", Usage(argument))
        UsageText(argument) -> #("usage_text", UsageText(argument))
        ArgsUsage(argument) -> #("args_usage", ArgsUsage(argument))
        Version(argument) -> #("version", Version(argument))
        Description(argument) -> #("description", Description(argument))
        DefaultCommand(argument) -> #(
          "default_command",
          DefaultCommand(argument),
        )
        Category(argument) -> #("category", Category(argument))
        Commands(argument) -> #("commands", Commands(argument))
        Authors(argument) -> #("authors", Authors(argument))
        Copyright(argument) -> #("copyright", Copyright(argument))
        Parent(argument) -> #("parent", Parent(argument))
      }
    })
    |> dict.from_list

  Command(name, args)
}

Might be worth building some kind of factory abstraction but I'd have to think on how I'd want that API to look as well

past wing
#

If there is a fixed set of fields, you shouldn't use a Dict. That removes all type safety from your code

#

Also it seems not all the fields have the same type, which would make it impossible to use a Dict here anyway

#

I would suggest using a builder pattern here

wet badger
#

Builder pattern or a default with a spread is definitely the way here

obsidian flare
#

writing setters or None for each is not ideal.

If you have heaps of fields nothing stops you from using code generation

shadow wolf
#

Builder pattern is cool! I'm just a student, this is the first time I've implemented it (and I see now that's what the previous commenters were recommending)

this is what I've got now:

pub type CommandArgument {
  Aliases(x: List(String))
  Usage(x: String)
  // ...
}

pub opaque type Command {
  Command(
    name: String,
    aliases: Option(List(String)),
    usage: Option(String),
    // ...
  )
}

fn init(name: String) -> Command {
  Command(
    name,
    aliases: None,
    usage: None,
    // ...
  )
}

fn add_aliases(cmd: Command, aliases: List(String)) -> Command {
  Command(..cmd, aliases: Some(aliases))
}

fn add_usage(cmd: Command, usage: String) -> Command {
  Command(..cmd, usage: Some(usage))
}

pub fn new(name: String, arguments: List(CommandArgument)) -> Command {
  let cmd = init(name)
  let _ =
    arguments
    |> list.try_each(fn(arg) {
      case arg {
        Aliases(arg) -> cmd |> add_aliases(arg)
        Usage(arg) -> cmd |> add_usage(arg)
      }

      Ok(arg)
    })

  cmd
}

because i want to abstract all logic away from the consumer it is a little verbose that i have to write out each type like 4-5 times (unless I'm missing the forest for the trees somewhere) but this feels "clean" for the time being

dense basin
#

iirc in the builder pattern you'd just expose the add_* functions and have a new function that just returns a default (with the required fields set)

shadow wolf
#

I think you're right, which would have the consumer write something like this

let cmd = cmd.new("foo")
|> cmd.add_aliases(["bar", "baz"])
|> cmd.add_usage("idk")

but I guess the idea in my head was to have a lot of this abstracted away

  let cmd = new("foo", [
    Aliases(["bar", "baz"]), 
    Usage("idk")
])

which is more in line with like a struct in another language.

Seeing it written out I'm not sure mine is actually all that much simpler/abstracted, but again I guess the underlying question is how would a Gleam programmer write this, and I guess also how would you expect to use this if you didn't write it?

dense basin
#

You could look at what other modules do, like gleam_http

#

I think the convention is the first

tropic ravine
# shadow wolf I think you're right, which would have the consumer write something like this `...

nobody is gonna like me for this but

pub opaque type Command {
  Comand(name: String, ...)
}

pub type Option = fn(Command -> Command)

pub fn new(name: String, options: List(Option)) -> Command {
  let init = Command(name, None, ...)
  use command, option <- list.fold(options, init)

  option(command)
}

pub fn aliases(list: List(String)) -> Option {
  fn (command: Command) {
    Command(..command, aliases: Some(list))
  }
}

pub fn usage(value: String) -> Option {
  fn (command: Command) {
    Command(..command, usage: value)
  }
}
#
new("wibble", [
  aliases(["w", "wib"]),
  usage("blah blah blah")
])
shadow wolf
# dense basin You could look at what other modules do, like gleam_http

it looks like they do leave building up to the consumer and use set_* as the naming system.

I went ahead and wrote it up this way and while it kind of feels like the "wrong" way to write OOP, it actually made the rest of the code super frictionless! Definitely have a lot to learn in this space.

I think I'm happy here, thanks everybody lucycat