V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
netabare
V2EX  ›  程序员

关于在 Java 里面实现命名参数的一些想法

  •  
  •   netabare · 77 天前 · 2571 次点击
    这是一个创建于 77 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近看到了一篇文章,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), ...);
    

    但似乎显得更怪异了,而且这么写代码感觉会被人打(

    8 条回复    2025-08-16 12:46:19 +08:00
    yazinnnn0
        1
    yazinnnn0  
       77 天前
    为啥不用 kotlin 或者 scala
    xtreme1
        2
    xtreme1  
       77 天前
    vavr 里有类似的东西
    用多了在 idea 中编辑时巨卡
    chendy
        3
    chendy  
       77 天前
    一个劣化版本

    public static void main(String[] args) {
    Demo student = new Demo(
    d -> d.setId(1L),
    d -> d.setAge(24),
    d -> d.setName("Student")
    );
    }

    @Data
    public static class Demo {
    private long id;
    private String name;
    private int age;

    public Demo() {
    }

    public Demo(Consumer<Demo>... consumers) {
    for (Consumer<Demo> consumer : consumers) {
    consumer.accept(this);
    }
    }
    }
    guyeu
        4
    guyeu  
       77 天前
    Java 没有默认参数、关键字参数这些功能,对命名参数也没有啥需求啊。。
    Ketteiron
        5
    Ketteiron  
       77 天前
    语言的语法是用来增强表达的能力,你这个设计不太好,增加了阅读/编写的复杂度,但是收益无法抵消额外开销,在我看来是负优化,想在 java 里搞奇技淫巧的应该直接转 kotlin/scala ,别指望能在垃圾语言上屎上雕花,只能屎上雕屎。
    命名参数这玩意,是为了把参数从水平转成垂直,就像浏览器的普通标签页 vs 垂直标签页,语法上想实现这种功能,一定要支持默认参数/可选参数,所以 java 只能洗洗睡了。
    而且命名参数也不是最好的设计,js 不支持命名参数,但是 es6 的参数解构远比命名参数好用。
    cloudzhou
        6
    cloudzhou  
       76 天前
    在业务开发方面,脚本语言几乎式微,一个原因就是维护性不好,写的开心维护的累
    Perl 更是誉为 readonly 语言
    So ,builder 已经足够可以了

    你这个模式是 Golang 的 Option 模式,用于某些选项类,不是通用参数
    netabare
        7
    netabare  
    OP
       76 天前
    @yazinnnn0
    @dssxzuxc 我自己写肯定直接选择 Dotty 或者 ML 了,只是看到有人讨论「怎么在 Java 里面引入命名参数」这个帖子,然后产生了一点脑洞而已。而且 @dssxzuxc 说的「屎上雕屎」这点我也很认同,我自己对命名参数这玩意本身也没很感冒。

    @chendy
    其实你这个想法感觉有点像「依赖注入」或者一个「劣化版本的 Scope 」了吧,我个人倒是感觉其实语法上和 builder 模式算是等价的?
    xuanbg
        8
    xuanbg  
       76 天前
    好玩,但没什么用
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   4140 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 05:33 · PVG 13:33 · LAX 22:33 · JFK 01:33
    ♥ Do have faith in what you're doing.