Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion around optional arguments to functions #60

Open
hinshun opened this issue Mar 30, 2020 · 3 comments
Open

Discussion around optional arguments to functions #60

hinshun opened this issue Mar 30, 2020 · 3 comments
Labels
design Design for a feature

Comments

@hinshun
Copy link
Contributor

hinshun commented Mar 30, 2020

As we develop common modules in: https://github.com/openllb/modules

We want to expose as many knobs to ensure that it is reusable. However, at the same time we want to limit the number of required arguments so that you can invoke the module without being exposed to the full spectrum of options.

There's two approaches I've been thinking about:

  1. Optional arguments
  2. Option signatures

Optional arguments

Starting with some implementations from some languages:

# Named arguments in ruby
def test(var1: "var1", var2: "var2", var3: "var3")
  puts "#{var1} #{var2} #{var3}"
end

test(var3:"var3-new", var1: 1111, var2: 2222) # ok => 1111 2222 var3-new
# Default values for arguments in ruby
def test(var1="var1", var2="var2", var3="var3")
  puts "#{var1} #{var2} #{var3}"
end

test(var3:"var3-new", var1: 1111, var2: 2222) # ok but ... {:var3=>"var3-new", :var1=>1111, :var2=>2222} var2 var3

or python optional arguments

def info(object, spacing=10, collapse=1):
  # ...

info(odbchelper)
info(odbchelper, 12)
info(odbchelper, collapse=0)
info(spacing=15, object=odbchelper)

The key features extracted from the examples is:

  • Positional arguments strictly before named arguments
  • Named arguments can be in any order
  • Values can be assigned to arguments even if they don't have default values set

In HLB, this may look like:

fs foo(string first) {
    # ...
}

fs bar(string first, string second="world") {
    # ...
}

group default() {
    foo "hello"
    foo first="hello"
    bar "hello"
    bar second="world" first="hello"
}

Option signatures

Currently, builtin functions like run can take options that add optional behavior to a function. And you can define functions that wrap these builtin options too:

option::run commonOptions() {
    env "key" "value"
    dir "/src"
}

fs default() {
    image "alpine"
    run "echo foo" with commonOptions
}

User defined functions cannot have options, which makes for a odd experience. Here's some explorations into how we can make this work:

Idea 1

fs foo(string requiredArg) {
    image baseImage="alpine:3.9"
}

fs default() {
    foo "hello" with option {
        baseImage "alpine:3.9"
    }
}

Idea 2

# Looking awful like optional arguments
fs foo(string requiredArg) optional (string baseImage="alpine:3.9") {
    image baseImage
}

fs default() {
    foo "hello" with option {
        baseImage "alpine:3.9"
    }
}
@hinshun hinshun added the design Design for a feature label Mar 30, 2020
@slushie
Copy link
Contributor

slushie commented Mar 31, 2020

One key difference between Ruby and Python vs HLB is that those languages already have support for maps and lists. So those optional arguments are data that can be passed around, splatted out with *args and manipulated by users and libraries. Using the syntax described above, users will not be able to perform composition operations on arguments, like manipulating the args from a wrapper function before passing them on.

For example, given the following function:

fs build(fs src, string os="linux", string arch="amd64") {
  image "golang"
  env "GOOS" os
  env "GOARCH" arch
  run "go build mypkg" with option {
      mount src "/go/src/mypkg"
  }
}

A wrapper function may want to specify the arch argument but leave the os argument in place, if defined. The syntax you describe makes that impossible; instead users would have to "copy up" the args they wrap in order to expose them to consumers.

fs buildOnArm(fs src, string os="linux") {
  build src with option {
    os os
    arch "arm"
  }

fs default() {
  buildOnArm someFs with option {
    os "darwin"
  }
}

IMHO, the best syntax to use here combines the with option block and optional clause. It may be prudent to add "Idea 2" syntax to expose arbitrary options externally. For example, spitballing syntax using the existing additive options:

fs buildOnArm(fs src, string arch="arm") optional (options build.options) {
  build src with options {
    build.options
    arch arch
  }
}
fs default() {
  buildOnArm someFs with option {
    # os is defined on build.options, which is part of the "optional" clause
    os "darwin"
  }
}

@hinshun
Copy link
Contributor Author

hinshun commented Mar 31, 2020

One key difference between Ruby and Python vs HLB is that those languages already have support for maps and lists. So those optional arguments are data that can be passed around, splatted out with *args and manipulated by users and libraries. Using the syntax described above, users will not be able to perform composition operations on arguments, like manipulating the args from a wrapper function before passing them on.

Good point, I'm still erring on the side of avoiding composite data types in an effort to keep the language simpler but it may be a line we have to cross at some point. For now, I'll try to continue with basic types.

For example, given the following function:

fs build(fs src, string os="linux", string arch="amd64") {
  image "golang"
  env "GOOS" os
  env "GOARCH" arch
  run "go build mypkg" with option {
      mount src "/go/src/mypkg"
  }
}

A wrapper function may want to specify the arch argument but leave the os argument in place, if defined. The syntax you describe makes that impossible; instead users would have to "copy up" the args they wrap in order to expose them to consumers.

Which proposal are you talking about? In the Optional arguments proposal, you can specify arch without os because the order doesn't matter in named arguments:

fs buildOnArm(fs src) {
    build src arch="arm"
}

IMHO, the best syntax to use here combines the with option block and optional clause. It may be prudent to add "Idea 2" syntax to expose arbitrary options externally. For example, spitballing syntax using the existing additive options:

fs buildOnArm(fs src, string arch="arm") optional (options build.options) {
  build src with options {
    build.options
    arch arch
  }
}
fs default() {
  buildOnArm someFs with option {
    # os is defined on build.options, which is part of the "optional" clause
    os "darwin"
  }
}

I like to idea of providing arbitrary options to pass through without manually copying up within each layer. I'm confused to the build.options because currently the selector expr is reserved for calling identifiers from imported modules.

@slushie
Copy link
Contributor

slushie commented Apr 1, 2020

In the Optional arguments proposal, you can specify arch without os because the order doesn't matter in named arguments:

Right, but the consumer of buildOnArm can't specify os unless buildOnArm also specifies it. That's the issue I'm addressing via build.options below.

currently the selector expr is reserved for calling identifiers from imported modules

Yeah you'd need to define new syntax to provide for the composition of optionals. In other words, right now there is no way to refer to a functions optionals; either some data type for maps needs to exist, or some syntax to refer to optionals needs to exist.

The build.options syntax isn't great, but it does allow users to refer to a function's optionals. Without it, users will always need to "copy up" those options.

For example, right now build takes os and arch optional args and buildOnArm can expose the os optional arg in its own signature, then plumb it back down to build. But tomorrow, build might change -- maybe it adds a goFlags optional arg as well. Now, buildOnArm must update its signature in order to allow its own consumers to specify that arg.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design for a feature
Projects
None yet
Development

No branches or pull requests

2 participants