Function environment parameters

An function environment parameter is a parameter that has the default value of the calling function environment parameter of the same name. If there is no environment parameter of the same name, the compile will fail with an error if the value is not specified.

func f1(e1: environment Int) {
    print(p1)
}

func f2(e1: environment Int) {
    f1() //e1: e1
}

func f3() {
    f2(e1: 3) // executes f2 with e1 of 3, which executes f1 with e1 of 3, which prints 3
    f2() //complie error
}

If the environment parameter is (T) -> (), this could would be a solution to function builders that is not limited to builder functions.

1 Like

This sounds a lot like Scala’s implicits. Aren’t they backing away from implicits in Scala in favor of more structured mechanisms?

They look similar, but the name of the implicit parameters should be required to be the same as well as the type. It looks like Scala only requires the type to match.

I was also thinking that only parameters could be environment, but there is no reason why they can't be local variables.

let e1: environment Int = 5
f2()

Maybe implicit is a better keyword, since these parameters are not global and are not from the OS environment.

The language itself isn't backing away from them (AFAIU), but they're considered a fairly advanced feature of Scala that most everyday programmers are discouraged from using. They can provide for some interesting APIs, but they can start to create hard to reason about interactions that require some foresight into how the APIs in question will be used in context.

I would not really be happy to see them in Swift. From my Scala experience, implicits are used when the API author had a particular vision for the API that was based around the implicit feature. And IMO most of these APIs are a bit too clever for their own good and would've probably been easier to learn if they were based around a bit more internal framework structuring.

2 Likes

Looking at the Scala documentation: https://docs.scala-lang.org/tour/implicit-parameters.html
I believe that the main issue is that Scala's implicit parameters only require types to match. That allows some weird dynamic behavior when combined with Generics. I don't want this behavior. The names of the variables/parameters should be required to match as well as the type.

@nuclearace is this where the issues with Scala implicit's come from?

I think if implicits were only used to pass basically global variables (that can change per call) from caller functions to called functions, they should be relatively simple to understand.

Partly. Things can get hectic if you're using multiple APIs that are both trying to use implicits. Which is why it's discouraged from using implicits with common types. It's still a common trap that new programmers will fall into though.

I think the bigger issue is that it's just an advanced feature with fairly niche cases that are truly useful. Most of the time the same affect can be achieved using alternative methods that are easier to learn and scale.

2 Likes

It could be quite useful for something like NSProgress and logging — and maybe a lot more things which either require globals or long parameter lists.

Current posible solutions:

Globals or static class members:
no visibility.
no compile error if not set
only 1 instance, however it can temporarily be changed for a function call, then changed back. This work around is not thread safe.

Long parameter lists:
hard to read
requires more maintenance when one is added or removed

Thread local variables:
no visibility
needs to be manually copied if a function executes code on a separate thread.
no compile error if not set
only 1 instance, however it can temporarily be changed for a function call, then changed back.
If this is implemented with Thread.current.threadDictionary, it will suffer naming conflicts.

Compared to implicit parameters:
minor visibility on the call site. There is an implicit variable, but it is not clear which functions use it, however the function definitions will state it clearly.
Naming conflicts if the implicit parameter name is the same for two separate libraries. Not sure if this is a big issue. Naming conflicts with different types should be a compile error. I need to think more about this.
NOTE: implicit parameters should also apply to closure parameters which has not been discussed yet.

Yes, there are currently ways to use this concept currently in swift, but they each have their own issues. Implicit parameters seems to have the least issues.

1 Like

What concret problem does it solve ?

1 Like

To me it looks like this would introduce confusion more than convenience. I'm also not sure what problem it would solve other than a few extra characters to type.

4 Likes

Hardly any feature implemented in Swift does more than saving a few keystrokes - and the pitch aims to be an alternative design for function builders, which is a very complex addition that might have much more potential for confusion. Also, it's 100% clear that it's a problem that Core wants to see solved, so imho the better question is "how exactly" instead of "what".

In general, I really think the "community" is often too daunting, and that encouragement would be the better attitude towards fresh contributions... there's plenty of time for negative feedback when this idea approaches the state of implementation (and my prediction is that won't happen anyways).

It's not clear to me how environment parameters as described here would enable functionality similar to function builders. Could you provide an example?

1 Like

Using one of the examples from this function builders evolution pitch, this is an implementation using implicit function parameters:

let html = Html {
  body {
    div {
      if useChapterTitles {
        h1(chapter + "1. Loomings.")
      }
      p("Call me Ishmael. Some years ago")
      p("There is now your insular city")
    }
  }
}

enum HtmlComponent {
  case body(children: [HtmlComponent])
  case div(children: [HtmlComponent])

  // I am using String for h1 and p due to simplicity, but probably better to use something that is ExpressibleByStringInterpolation so it supports html tags.
  case h1(string: String)
  case p(string: String)
}

func body(_ children: (htmlBuilder: implicit HtmlBuilder)->(), htmlBuilder: implicit HtmlBuilder) {
  //I know closure parameters cannot be named currently, but this implicit parameters require named parameters due to how they need to be linked.
  let builder = HtmlBuilder()
  children(htmlBuilder: builder)
  htmlBuilder.append(.body(children: builder.children)
}

func div(_ children: (htmlBuilder: implicit HtmlBuilder)->(), htmlBuilder: implicit HtmlBuilder) {
  let builder = HtmlBuilder()
  children(htmlBuilder: builder)
  htmlBuilder.append(.div(children: builder.children)
}

func h1(_ string: String, htmlBuilder: implicit HtmlBuilder) {
  htmlBuilder.append(.h1(string))
}

func p(_ string: String, htmlBuilder: implicit HtmlBuilder) {
  htmlBuilder.append(.p(string))
}

class HtmlBuilder {
  var children: [HtmlComponent]
  func append(component: HtmlComponent) {
    children.append(component)
  }
}

struct Html {
  var children: [HtmlComponent]
  init(_ children: (htmlBuilder: implicit HtmlBuilder)->()) {
    let htmlBuilder = HtmlBuilder()
    children(htmlBuilder: htmlBuilder)
    children = htmlBuilder.children
  }
}
1 Like

This approach does not meet the design goals of function builders for the same reasons that receiver closures don't meet the design goals. It does not allow the builder to preserve type information the way function builders do. I recommend reading the comments on this topic made in the function builders thread by John McCall and others.

After some thinking on the type information, that solution should be separate. An idea I have is to use a tuple builder

class TupleBuilder<T> {
    append<V>(_ value: V)
    build() -> T
}

TupleBuilder would have a requirement that it cannot escape because the T is determined at compile time based on the number of appends and the types.

Special handling would need to be given to loop/condition statement, but by limiting its use, it is possible to know what T is at compile time.

HtmlBuilder could extend TupleBuilder, therefore limited to the same constraints, yet it will preserve the type information (without limiting it to an arbitrary size of 10 elements).

If this is liked, a separate pitch should be raised with this, as it is solving a completely separate problem as this pitch.