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

Add NamedTuple#update(other : NamedTuple) to merge values but keep type #4727

Conversation

straight-shoota
Copy link
Member

@straight-shoota straight-shoota commented Jul 19, 2017

This method is similar to #4688 and #2634 with different semantics as proposed in #4688 (comment):

NamedTuple#update(other : NamedTuple) creates a new instance of self's NamedTupe type with values of self updated with those of otherother's keys need to be a subset of self's keys. Inversion is only valid if the key sets are identical. The behavior is similar to #merge but it ensures that no additional keys are added and preserves the types from self.

A typical use case would be merging a fixed set of options with default values while disallowing additional keys (in contrast to #merge)

def do_something(*options)
  default_options = {foo: "bar", baz: 15.5 }
  updated_options = default_options.update(options) #=> {foo: "bar", baz: 2 }
  # ...
end

do_something baz: 2

I am not super happy with the name, because it does not actually update self but returns a new NamedTuple. So if there are any suggestions...

@akzhan
Copy link
Contributor

akzhan commented Jul 19, 2017

Looks like better to name:

updated_options = default_options.with(options)
# or
updated_options = default_options.apply(options)

@oprypin
Copy link
Member

oprypin commented Jul 23, 2017

In my opinion, there should be one method of this kind, the most universally useful one (#4688). The fringe benefits don't justify the complexity.

@ysbaddaden
Copy link
Contributor

NamedTuple is a mean to easily pass hash-like data around, for methods that may or may not consume passed values. The #merge and #merge! methods are enough to handle these cases.

If you want more strictness, you should define an explicit configuration struct.

@straight-shoota
Copy link
Member Author

straight-shoota commented Jul 24, 2017

Custom structs cannot easily be passed as method arguments. I think this is an important use-case for configuration/options as a NamedTuple. When you use a non-strict merge and accidentally add some additional keys, this will only fail later when the merged tuple is used as a options splash, not at the point where the wrong key/value was introduced.

@Sija
Copy link
Contributor

Sija commented Sep 29, 2017

Can we close this since #4688 was merged?

@asterite asterite closed this Sep 29, 2017
@straight-shoota
Copy link
Member Author

straight-shoota commented Sep 30, 2017

Why was this closed? This PR was intended to be merged alongside #4688 and #2634. They're not per se mutually exclusive but could live side by side with different semantics as in #4688 (issue comment).

I don't want to put to much pressure on this, but I don't think it's a reasonable explanation to close this and #2634 just because #4688 was merged. They propose to do something entirely different and imo provide a substantial benefit for working with named parameter options and default values. A real example is merge_options in sass.cr:
Basically, Compiler#compile allows to specify options as named parameters. They are collected in a NamedTuple and need to be merged with the default values from OPTIONS. merge_options is a lousy workaround and could easily be replaced by OPTIONS.update(options).
#4688 doesn't help for this use case at all.

@RX14 RX14 reopened this Sep 30, 2017
@bcardiff
Copy link
Member

@straight-shoota I don't follow why the NamedTuple.merge is not enough to handle options with default values.

A tuple with additional entries can/should be used always where a tuple with less entries is required. They can just not be used and be ignored and the program should make sense.

The only place where the compiler can't use a tuple with additional entries is when splat is introduced and the method has strict named argument. In this case the compiler will complain on the splat call and should be enough guidance to fix the issue.

Why does extra options complicate things in your scenario?

@Sija
Copy link
Contributor

Sija commented Sep 30, 2017

@bcardiff I think that main issue here is keeping the same type for a given :key, before and after the merge, see below:

def foo?(**options)
  default_options = {
    unicorn: true,
  }
  options = default_options.merge(options)
  bar!(**options)
end

def bar!(unicorn : Bool)
  if unicorn
    puts "Yay!"
  else
    raise "Oh no!"
  end
end

foo?(unicorn: 0xdeadbeef)

Since options[:unicorn] changed its type from Bool to Int64, the code above would fail with error:

Error in line 17: instantiating 'foo?()'

in line 6: no overload matches 'bar!' with types , unicorn: Int64
Overloads are:
 - bar!(unicorn : Bool)

@straight-shoota
Copy link
Member Author

@bcardiff I'm not sure if merge would be enough. But I'll try to figure that out.
And I'm also not sure if this proposal is a good thing. Especially because the naming is really difficult. update is very inaccurate, apply is not really better. Maybe something like with_values would be better suiting.

@RX14
Copy link
Contributor

RX14 commented Oct 2, 2017

I'm actually not sure that adding this method simply to error a bit earlier in type checking is a very good idea. I think that using named tuples as "options objects" should be quite rare with named arguments, and i'm not too sure of a usecase outside that. I don't think we should add this method if this is the only usecase.

@straight-shoota
Copy link
Member Author

Okay, I think we can close this. There seems to be not much benefit from this method.
But thanks for having the discussion!

@bcardiff
Copy link
Member

bcardiff commented Oct 2, 2017

@straight-shoota one last though about this. I think that early type checking the options is a bit too much. But if one could do that something like

alias Config = NamedTuple(host: String, port: Int32)

opt1 = { host: "localhost", port: 80 }
Config.new(**opt1)

opt2 = { host: "localhost", port: "80" }
Config.new(**opt2) # compile error

Adding extra options will also generate a compile time error. Which I don't like for this use case since it should not bother. To solve that what we could be missing is a slice method for tuples/namedtuple that would allow to project a reduced tuple with type information. But those helpers are doable upon need.

@straight-shoota straight-shoota deleted the named-tuple-update branch November 18, 2021 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants