On Sun, Oct 21, 2018 at 2:03 PM roger peppe <rogpe...@gmail.com> wrote:

> Yes. The draft proposal says "generic functions, rather than generic
> types, can probably be compiled using an interface-based approach. That
> will optimize compile time, in that the package is only compiled once, but
> there will be some run-time cost.". I don't think this is possible as
> stated. I don't even know what it means to compile a generic type, as types
> themselves aren't compiled - unless they're talking about compiling methods
> on generic types, in which case I don't see how they differ from generic
> functions.
>
> I do believe it's possible to avoid generating code for every possible set
> of type parameters to a generic function. There's a continuum of
> efficiency. At one end, we *could* generate code for each possible set of
> type parameters (giving efficient code but binary bloat), and at the other,
> I think it's possible to generate a function containing reflect-based code
> that will work on all possible type parameters (*) but is comparatively
> very slow.
>

To me, it seems that you are contradicting yourself here. To clarify: To
me, an "interface-based approach" and "reflect-based code" essentially
comes down to exactly the same thing: We pass run-time type information to
the function which then unpacks that to do the actual work. Tomato Potahto
;)

(and FWIW "compiling a type", I believe, refers to generating a
reflect.rtype/runtime._type at compile time and embedding it in the binary.
So, if you have `type Foo(T) struct { x T }`, an interface-based
compilation would just generate one run-time type of the form `type Foo
struct { x interface{} }`, whereas a static approach would create e.g.
`type Foo(int) struct { x int }`, `type Foo(string) struct { x string }`,
etc)

But, I guess, I'm fine to agree to disagree at that point then - if you
think a purely run-time/reflect/interface/… approach is impossible, I can't
argue with that :)

Somewhere in the middle, is probably a happy medium: perhaps something like
> "generate a version of the function for all types with the same size and
> pointer map".
>
> (*) some things aren't currently possible, like iterating over maps in
> O(1) space, but it's likely they can be solved in time.
>
> To answer your question: I handwavily put that under the same "operator"
>> headline of a) (lord, all of this would be so much easier with a formal
>> type system. That was the intention of a) - restrict the set of
>> type-constructors we have to talk about to func). An extremely simple way
>> would be
>>
>> type A = interface { … }
>> type R = interface { … }
>> func G(a func() []A) *R
>>
>> and at the call site
>>
>> var x []CA
>>
>
> Your code here assumes that x is known at the call site. That's not a
> valid general assumption. The function might be well be generating the
> slice on every call.
>
> var y *CR
>> _y := G(func() []A {
>>     var out []A
>>     for _, a := range x {
>>         out = append(out, a)
>>
>
> Making a copy of the slice isn't valid in general either, as the slice
> might legitimately alias some other slice elsewhere in the program. When G
> mutates the slice, it's important that the elements that it mutates are the
> same ones as in the slice originally returned by the function.
>
> By way of example, think about how your transformation could work on this
> code: https://play.golang.org/p/_nfS2rEJIJy
>
>
>>     }
>>     return a
>> })
>> if _y != nil {
>>     tmp := (*_y).(CR)
>>     y = &tmp
>> }
>>
>> For the full argument, we'd also need that the compiler is able to
>> recognize and reverse these patterns. There are ways to do this
>> transformation that make this reversal simpler (if you transform
>> slice-accesses in the parametric function into method calls and generate
>> appropriate wrappers, the reverse transformation becomes simply inlining
>> and the trivial devirtualization), but I'm too lazy to write them down now.
>>
>
> I don't understand why you think the compiler needs to be able to
> recognize and reverse these patterns.
>
> For the record, I'm fully aware of the problems with this approach
>> <https://blog.merovius.de/2018/06/03/why-doesnt-go-have-variance-in.html>
>> in general. However, the big advantage we have here is that we can ignore
>> type-safety. This is an internal optimization after the type-checking pass
>> that has already guaranteed that what we're doing is safe. Which is pretty
>> much the crux of b) - being able to ignore type-safety allows us to do
>> abhorrent, reflect/unsafe based things that we wouldn't want to do in the
>> language proper but which allows us to just assume any co-/contravariance
>> we need.
>>
>
> I don't think it's possible to assume co-/contravariance even with unsafe,
> because the runtime representation of interface values really is different
> from that of concrete values. The only way of doing this would be to make
> all values in the program the same size, which is indeed the approach taken
> by many languages, so []byte would have exactly the same layout as
> []io.Reader, for example. I don't think this is an acceptable approach for
> Go though.
>
> c) Given a function
>>>> func F(a A) R { … }
>>>> with A, R interface types and concrete types CA, CR implementing A and
>>>> R respectively and assuming every return statement of F can be proven to
>>>> return a value of dynamic type CR, we can create a devirtualized version of
>>>> F with the signature
>>>> func CF(CA) CR
>>>> such that for any a, the expression `F(a).(CR)` is equivalent to
>>>> `CF(a)` and the expression `F(a)` is equivalent to `R(CF(a))`.
>>>> (and similarly for the func value CF when passing it around)
>>>>
>>>
>>> "assuming every return statement of F can be proven to return a value of
>>> dynamic type CR". This is a big assumption, and can only be done with flow
>>> analysis and then only in a limited set of cases. For cases where you can't
>>> prove it, you can't make devirtualized version of F.
>>>
>>
>> It may be a big assumption - but that's not what c) is about. c) is the
>> lemma "assuming we have this proof, no matter where it came from". d) is
>> then the lemma that at least the cases resulting from b) fall within that
>> "limited set of cases" :) So, for clarity: Is this your only problem with
>> c)? Because then I'm going to assume we are discussing d).
>>
>> Again, I agree with you that, *in general*, obtaining this proof is
>> non-trivial. But we don't have to care about the general case, we only have
>> to care about the case where the function resulted from our self-devised
>> generic rewrites, which gives us a very good idea of how these functions
>> look and what to look out for.
>>
>
> Originally you claimed that "the analysis [of the exact set of possible
> types that a type parameter can be] for interfaces is [...] *exactly as
> hard *[as that for generic type-parameterized functions]".
>
> As I understand it, this means that you're claiming that taking some
> arbitrary piece of Go code that uses interfaces and determining the exact
> set of possible dynamic types that those interfaces can be is *exactly as
> hard* as taking a generic function and determining the set of possible
> types it can be called with. I guess I must be misinterpreting you here,
> because it's quite easy to see that if I have a method Foo:
>
>     type T struct {
>         I interface { A() }
>     }
>     func (t *T) Foo() {
>         t.I.A()
>     }
>
> then it's only possible to know what types t.I can be by knowing where all
> possible references there might be to t.I in the entire program and knowing
> what dynamic types it might be set to. This is the problem solved by Guru's
> "pointsto" query - it's slow and hard and inaccurate (to do it right, you
> really have to trace reflect-based code too).
>
> Note, that if we *absolutely* had to, we could even add a fast-path to
>> whatever heuristic we come up with by keeping the information from b)
>> around (e.g. in a special `//go:` directive - or just a
>> `map[FuncDecl]ParametricFuncDecl`). As long as we don't *solely* rely on
>> that, this wouldn't weaken the overall argument. But I'm pretty sure that
>> the fact that we know what transformations we did in b) makes writing this
>> heuristic easy(ish).
>>
>
> Ah, I *think* I see where you might be coming from now. You're thinking
> that the compiler could do some kind of Go-to-Go transformation for generic
> functions so that they looked exactly as if they'd been written using
> interfaces, and then later transform that back into devirtualized calls.
> Even if it were possible in general (I'm fairly sure it's not), I don't see
> why anyone would want to take such an indirect approach when it's quite
> possible for the compiler to compile to an intermediate format that
> preserves all the information needed and decide how to generate the code
> based on that.
>
>

-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to golang-nuts+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to