最近看到了一篇文章,Approximating Named Arguments in Java,讲的是怎么在 Java 里面模拟类似 Python 的命名参数,整体来说不算太出乎我的意料吧,主要还是原地改状态和 builder 模式,还有个 with 的写法虽然看起来有点意思,但总感觉缺点什么。
不过看到这个风格的写法的时候,突然想起了之前自己写的一个项目里面用 lambda 来模拟模式匹配的写法。
kMeans(X, nClusters, opts -> opts.maxIter = 1000);
String guess = new Match<Shape, String>(new Circle())
  .is(Circle.class, (circle) -> "我是一个圆")
  .is(Square.class, (square) -> "我有点方")
  .otherwise((Shape shape) -> "你看不见我")
  .get();
然后突然想,是不是可以用一个 lambda 参数来当占位符,来变相实现命名参数。大概是(maxIter -> 1000)这样的感觉。
但仔细想的话,Java 里面没有 currying 和 uncurrying 的概念,换句话说,本来我构想的在 Java 里应该用的是 Supplier 接口,但现在我手头上的是 Function<T, U>而且其中的 T 无法被忽略掉。似乎是一个很致命的问题。
看起来就卡死了……
然后想了想,其实我需要的是一个能把 Function<T, U>消掉的东西,回忆了一下最近 code review 从隔壁 senior 学来的 static class 的一些奇怪的写法,然后又想了想自己最近在搞的 Ref<T>,然后折腾出了一坨这玩意:
static final class KMeans {
    
    static final class Options {
        int maxIter = 300;
        double tol = 1e-4;
        boolean verbose = false;
        // ...
    }
    
    @FunctionalInterface
    interface Param {
        void apply(Options o);
    }
    
    static Param maxIter(IntSupplier s) {
        return o -> o.maxIter = s.getAsInt();
    }
    
    static Param tol(DoubleSupplier s) {
        return o -> o.tol = s.getAsDouble();
    }
    
    static Param verbose(BooleanSupplier s) {
        return o -> o.verbose = s.getAsBoolean();
    }
    
    static Output kMeans(Data X, int nClusters, int maxIter, boolean verbose, double tol, ...) { ... } // 这里是具体的代码逻辑
    
    static Output kMeans(Data X, int nClusters, Param... params) {
        var options = new Options();
        Arrays.stream(params).forEach(applier -> applier.apply(options));
        
        // 然后就可以继续实现具体的代码逻辑了
    }
}
psvm 里面可能长这样:
var X = new Data(...);
var res1 = KMeans.kMeans(X, 3, 1000, 1e-8, ...); // 常见的写法
var res2 = KMeans.kMeans(X, 3, 
                        KMeans.verbose(() -> true),
                        KMeans.tol(() -> 1e-6), 
                        KMeans.maxIter(() -> 1000), 
                        ...); // 现在可以这么写了,而且顺序也可以换来换去
                        
// 如果不用担心 import 会变得乱七八糟的话还可以这样
var res3 = kMeans(X, 3,
                 maxIter(() -> 1000), tol(() -> 1e-6), verbose(() -> true), ...);
感觉看起来有点像 Python 的命名语法,也不至于像 builder 模式那么啰嗦了。
但是总感觉好像还是有点啰嗦(
然后还可以稍微再改改。
interface Constant {
    static Param maxIter(int maxIter) {
        return o -> o.maxIter = maxIter;
    }
    static Param tol(double tol) {
        return o -> o.tol = tol;
    }
}
// 用法
IntSupplier someOtherCalc = () -> { return ... ; } // 假设它会返回 2048
var res4 = kMeans(X, 3, 
                  maxIter(someOtherCalc)
                  Constant.tol(1e-6),
                  Constant.verbose(true), ...);
但似乎显得更怪异了,而且这么写代码感觉会被人打(
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.