【问题标题】:Restricting a generic parameter in the scope of a method限制方法范围内的泛型参数
【发布时间】:2018-12-26 14:01:31
【问题描述】:

假设我们有一个class Foo<T>,它表示一些操作,它产生一个T 类型的值。我希望能够组合这个类的两个对象,以防一个产生Function<A, B>,第二个产生A 以获得Foo<B>。我可以使用以下静态方法来做到这一点:

public static <T, U> Foo<U> compose(Foo<Function<T, U>> fn, Foo<T> x) {
    // the composition is an implementation detail
}

现在我可以轻松地将Foo&lt;Function&lt;A, B&gt;&gt; 类型的fooAtoBFoo&lt;A&gt; 类型的fooA 组合起来,如下所示:

compose(fooAtoB, fooA)

不幸的是,在我的情况下,Foo 生成像 Function&lt;A, Function&lt;B, Function&lt;C, D&gt;&gt;&gt; 这样的嵌套函数是很常见的,我希望能够用 Foo&lt;A&gt;Foo&lt;B&gt;Foo&lt;C&gt; 组合这些函数来获得 @ 987654337@.

我可以通过以下方式做到这一点:

compose(compose(compose(fooAtoBtoCtoD, fooA), fooB), fooC);

这可行,但有很多嵌套,因此可读性受到影响。理想情况下,我想做的是这样的:

fooAtoBtoCtoD.compose(fooA).compose(fooB).compose(fooC)

但据我所知,不可能表达这样的事情,因为无法命名compose的返回类型,因为compose无法知道T是否在Foo&lt;T&gt; 实际上是一个Function&lt;U, V&gt;

换一种方式很容易做到:

<U> Foo<U> compose(Function<T, U> fn)

但这又不是很好用。

fooC.compose(fooB.compose(fooA.compose(fooAtoBtoCtoD)))

参数是从右到左写的,有很多嵌套。

这让我想到了这个问题:

有没有办法将FooT 参数限制为仅在compose 方法范围内的类型子集,以便它可以利用该限制?

能让我表达以下内容的东西会很棒:

T extends Function<U, V> in the scope of
<U, V> Foo<V> compose(Foo<U> x) {
    // values of type T are convertible to Function<U, V>
    // in the scope of this method
}

现在我认为没有这样的东西存在,并且T是一个函数的证明必须来自外部调用compose时,所以是这样的:

<U, V> Foo<V> compose(Function<T, Function<U, V>> fn, Foo<U> x) {
    // I can use fn#apply to convert a T to a Function<U, V> here   
}

在给定恒等函数时工作正常:

fooAtoBtoCtoD.compose(x -> x, fooA).compose(x -> x, fooB).compose(x -> x, fooC)

但这有点太长了,一遍遍重复x -&gt; x 并不好玩。

解决方案可能是将函数与Foo 一起打包。给定以下类:

class FooWithIdentity<T, U> extends Foo<T> implements Function<U, U> {
    @Override
    U apply(U val) {
        return val;
    }

    ...
}

以及Foo中的以下两种方法:

<P extends Foo<U> & Function<T, Function<U, V>>, U, V> Foo<V> compose(P foo)
{
    Function<U, V> fun = foo.apply(value);
    return new Foo<>(fun.apply(foo.value));
}

<U> FooWithIdentity<T, U> wrap() {
    return new FooWithIdentity<>(this);
}

我实现了以下目标:

fooAtoBtoCtoD.compose(fooA.wrap()).compose(fooB.wrap()).compose(fooC.wrap())

在我看来,这是迄今为止最好的解决方案。

编辑: Radiodef 的解决方案让我想到了一个具有相同界面的更简单解决方案:

class Foo<T>
{
    ...
    <U> U through(Function<Foo<T>, U> fn) {
        return fn.apply(this);
    }

    <U> Function<Foo<Function<T, U>>, Foo<U>> composing() {
        return fooFn -> /* compose fooFn and this here */;
    }
}

本质上,through 只是接受一个函数并通过它运行thiscomposing 生成一个执行实际组合的函数,它们可以像这样一起使用:

Foo<D> fooD = fooAtoBtoCtoD
    .through(fooA.composing())
    .through(fooB.composing())
    .through(fooC.composing());

【问题讨论】:

    标签: java generics api-design


    【解决方案1】:

    另一个变体是制作例如以下方法:

    class Foo<T> {
        ...
        <U> Foo<U> map(Function<T, U> fn) {
            return new Foo<>(fn.apply(this.value));
        }
        static <T, U> Function<Function<T, U>, U> composing(Foo<T> t) {
            return fn -> fn.apply(t.value);
        }
    }
    

    这会让你做例如这个:

    Foo<D> fooD =
        fooAtoBtoCtoD.map(composing(fooA))
                     .map(composing(fooB))
                     .map(composing(fooC));
    

    (Here's a working example.)

    这实际上并没有更短,但它更清楚它在做什么。

    至于abc.compose(a).compose(b) 之类的希望,不幸的是,目前不在 Java 中。

    我认为 Java 编译器原则上可以支持某种实例方法级别的泛型,例如 &lt;A,B&gt; where 'this' must be a Foo&lt;Function&lt;A,B&gt;&gt;。它只需要在方法调用站点检查引用的类型,因此不会像擦除一样阻止它。

    通常当 Java 缺少泛型功能时,这是因为擦除使其无法实现,例如泛型构造函数调用,但我认为这里不是这种情况。方法体(要求 TFunction)可以擦除为像 ((Function) this.value).apply(a) 这样的强制转换。

    【讨论】:

    • 这是一个合理的解决方案,尽管它并非没有问题 - composing 需要能够将 Foo&lt;T&gt; 直接转换为 T,这在我的情况下是不可能的,作为组合需要通过每个Foo 传递额外的状态。但这给了我一个想法,请参阅上面的编辑。另外,我同意你的最后三段,我也很确定这在技术上可以在语言本身中完成,而无需扩展 JVM。
    • 如果您觉得这个想法有用,我将不胜感激。如果您最终在编辑(或其他)中使用了该解决方案,您也可以考虑将其发布为答案。
    • 点赞。我会考虑其他解决方案,如果我无法提出更好的解决方案,我会发布自己的答案。
    猜你喜欢
    • 2015-02-23
    • 2023-03-20
    • 1970-01-01
    • 1970-01-01
    • 2010-10-27
    • 1970-01-01
    • 2018-01-22
    • 2010-11-22
    • 1970-01-01
    相关资源
    最近更新 更多