#612 Change signature of List.map and Map.map

tompalmer Wed 27 May 2009

Why does List#map have a parameter for appending to the list? Seems unexpected to me. Is it a common need? For example, the example in the docs shows what seems to be pain for the common case of simply mapping a list:

list.map(Int[,]) |Int v->Obj| { return v*2 }

I think I'd rather say just this:

list.map |Int v->Obj| { return v*2 }

(Or just this, really: list.map {it * 2})

brian Thu 28 May 2009

It takes a parameter so that you can type the list or append to an existing list. I agree it is a bit awkward, but couldn't think of anything else better.

tactics Thu 28 May 2009

I think tompalmer's way is the more natural way. If the append function is really necessary, perhaps we can move it to List#appendMap and change List#map to match this proposal.

brian Thu 28 May 2009

But then the resulting list will always be typed as Obj?[].

tactics Thu 28 May 2009

Ah, I misread your reply. I think I remember someone explaining that a while back.

tompalmer Thu 28 May 2009

But then the resulting list will always be typed as Obj?[]

Hmm. Thanks for clarifying. I think there may be a few other options. In each case, I'm recommending taking away the append feature here. I think it is rather confusing. I'd personally recommend it be a plus or plusList method. It wouldn't be far off from how plus works for Str.

Given that separated feature, here are my recommendations so far (some requiring more serious surgery than others):

  1. Take a resulting list type, as in ints.map(Str[]#) {"s" + it}.
  2. Take the element type for the result, such as what you'd get from of after the fact: ints.map(Str#) {"s" + it}.
  3. Allow optional parameters before final function blocks (I think this was discussed before?) so that, for instance, map could default to the current type. Note that this example is based on unchanging of, unlike the others in my list: ints.map {2 * it}. The method sig would look like so: List map(Type resultOf := this.of, |V, Int -> Obj?| c) or something like that.
  4. Use the return type of the function: ints.map |i -> Str| {"s" + i}. Note that I cheated and used partial function type inference here.
  5. Cheat further and infer the return type from the function contents, if unspecified (not sure whether Fan can do this today or whether it should): ints.map {"s" + it}.

I think I vote for 4, if you are up for that kind of feature, and if not, I think I vote for 3, if you are up for non-last defaults, or if not, then I vote for 2.

And in any case, I recommend making the docs clearer as to the meaning of the type parameter.

Anyway, just some thoughts.

brian Thu 28 May 2009

Inferring from the function return type would be cool, although probably would go against common conventions/idioms of using closure type inference.

Passing a type literal instead of an actual list is fine by me, I don't particularly have a strong opinion. I'd like to hear what others have to say.

Eventually we'll allow passing a closure after default parameters, but not for 1.0 (it is a big feature with lots of side effects).

tompalmer Fri 29 May 2009

Anyone else have opinions on these options?

andy Fri 29 May 2009

I could go with passing the type literal in, the code would be a bit cleaner.

KevinKelley Fri 29 May 2009

I agree that it is a little awkward, and I can't think of other places that work that way so it's not like it's a convention.

I don't see a big difference between passing a list to be filled, or passing a type literal. Either is kind of redundant, since you need to declare the return type in the closure declaration anyway (or is that inferred away?).

m := list.map(Str[,]) |obj->Str| { return obj.toStr }

Be nicer not to have to mention Str twice, is all.

As to the append feature in this, there's already a List.addAll(List) method, so no loss that I can see.

Eventually we'll allow passing a closure after default parameters, but not for 1.0

Is there even anybody using that accumulator parameter, or could it just be nixed completely and let map just return a new list? That seems more to me like it matches the style of everything else in List.

andy Fri 29 May 2009

As to the append feature in this, there's already a List.addAll method, so no loss that I can see.

Yeah, I agree, as little as I've used that (which is zero), its fine to handle with List.add. I could get on board with use the return type to create a new typed list.

tompalmer Fri 29 May 2009

I could get on board with use the return type to create a new typed list.

In Fan today (I think), for my example above, that would be:

ints.map |Int i -> Str| {"s" + i}

The type literal instead would be:

ints.map(Str#) {"s" + it}

I mostly prefer against the appended list as it stands today because the combined feature set is unexpected and confusing, at least to me.

Using the return type would be cleaner if there were more intense function type inference than what Fan has today. But without that, I think the type literal is easier to use (unless you usually want an Obj?[] result).

brian Sat 30 May 2009

Promoted to ticket #612 and assigned to brian

I'll change it to just type the returned list based on the return type of the function. Using type inference will just return Obj?[]:

list := ["a", "b", "c"]
list.map |Str x->Int| { x[0] } =>  Int[]
list.map |x->Int| { x[0] }     =>  Int[]  // assuming get partial inference working
list.map |x| { x[0] }          =>  Obj?[]
list.map { x[0] }              =>  Obj?[]

Having the list typed wrong isn't a huge deal, since you can cast it to anything. It really only matters if using reflection/serialization.

tompalmer Sat 30 May 2009

Sounds great. Side note, in the matrix library I've been toying with, I have to admit that I've been leaning to deciding types from function return types, too. This decision here will keep me leaning that way.

Using type inference will just return Obj?[]

I've been thinking that it might not be terrible to enhance return type inference to allow more specific types based on the types of the various return expressions. Really, it would be very much like type inference for Map and List literals. I think it is worth considering (and before 1.0, since it would not really be backwards compatible).

brian Sat 30 May 2009

I've been thinking that it might not be terrible to enhance return type inference to allow more specific types based on the types of the various return expressions.

I am thinking about that too. I need to sleep on that one though.

tompalmer Sat 30 May 2009

I thought about something tricky with using the return type. Say I make a method that calls List#map:

List callMap(List list, |Obj?, Int -> Obj?| mapper) {
  list.map |Obj? val, Int i -> Obj?| {
    // Just ridiculous changes here to justify wrapping the function.
    mapper(val, list.size - i - 1)
  }
}

So, here's my question: How can I write this function to control the return type seen by List#map? Seems extra tricky since it could be a different return type each time in this method.

My example above is contrived, but I hit an actual use case where I need to wrap a function in another, and I want the return type to be propagated.

tompalmer Sat 30 May 2009

Maybe some kind of Func constructor would work:

List callMap(List list, |Obj?, Int -> Obj?| mapper) {
  list.map(Func([Obj?#, Int#], mapper.returns) |Obj? val, Int i -> Obj?| {
    // Just ridiculous changes here to justify wrapping the function.
    mapper(val, list.size - i - 1)
  })
}

If there isn't a way to handle this need today and if the above technique seems interesting, I'll break it out to a new thread.

brian Wed 3 Jun 2009

Renamed from Append feature in List#map to Change signature of List.map and Map.map

brian Wed 3 Jun 2009

Ticket resolved in 1.0.44

I've changed the signature of both List.map and Map.map to take only a function. A new list/map is returned based on the return type of the function.

tompalmer Wed 3 Jun 2009

Sounds great. I'll break out my request for dynamically defined function types into a new thread.

Login or Signup to reply.