Conditional NamedTuple merge

Say I have the following code:

opts = {title: "abc", header: "test", value: 42}
popts = {title: "xyz", width: 10}

new_opts = opts.merge(popts)
p! new_opts # => {header: "test", value: 42, title: "xyz", width: 10}

How can I merge opts and popts while excluding popts keys missing in opts ?
Expected result :

# => {header: "test", value: 42, title: "xyz"}

EDIT: Solved by creating an update_from method in struct NamedTuple, adapted from the merge method, but as monkey-patching is not recommanded, is there another solution ?

Define your own struct to represent this data and define a custom #merge method on it.

2 Likes

You could define it as a non-member method:

def update_from(dst : NamedTuple, from src : NamedTuple)
  update_from_impl(dst, src)
end

def update_from_impl(dst : T, from src : U) forall T, U
  {% begin %}
    {% u_keys = U.keys %}
    NamedTuple(
      {% for k in T.keys %}
        {{ k.stringify }}: typeof(
          dst[{{ k.stringify }}],
          {% if u_keys.includes?(k) %}
            src[{{ k.stringify }}],
          {% end %}
        ),
      {% end %}
    ).new(
      {% for k in T.keys %}
        {% receiver = u_keys.includes?(k) ? "src" : "dst" %}
        {{ k.stringify }}: {{ receiver.id }}[{{ k.stringify }}],
      {% end %}
    )
  {% end %}
end

p update_from(
  {title: "abc", header: "test", value: 42},
  {title: "xyz", width: 10},
)

Unfortunately you cannot infer the NamedTuple instantiation in one step via NamedTuple(**T) forall T, in contrast to the working Tuple(*T) forall T.

Thanks a lot, @HertzDevil, that’s exactly what I was looking for (and besides, I now have some non-trivial code to study :wink:)!

If use ruby, you can get expected result use

opts = {title: "abc", header: "test", value: 42}
popts = {title: "xyz", width: 10}
result = opts.merge(popts.slice(*opts.keys))

Unfortunately, Crystal no NamedTuple#slice equivalent, you can create a feature request for this.

struct NamedTuple
  def slice(*keys : *U) forall U
    h = self.to_h.select { |x| keys.map(&.to_s).includes? x.to_s }

    h
  end
end

x = {x: 100, y: 200, z: 300}
p x.slice("x", "y")

# => {:x => 100, :y => 200}

Above method return a Hash, i thought it is hard to convert above h back to a tuple, i don’t know how to do that.