如何在不使用可变变量的情况下编写有用的Java程序


12

我正在阅读一篇有关函数式编程的文章,作者在其中指出

(take 25 (squares-of (integers)))

请注意,它没有变量。实际上,它只具有三个功能和一个常数。尝试在Java中写整数平方而不使用变量。哦,也许有一种方法可以做到,但是它当然是不自然的,而且读起来也不如我上面的程序那么好。

用Java可以实现吗?假设您需要打印前15个整数的平方,是否可以在不使用变量的情况下编写for或while循环?

Mod通知

这个问题不是代码高尔夫球比赛。我们正在寻找可以解释所涉及概念的答案(理想情况下,无需重复先前的答案),而不仅是针对另一段代码。


19
您的函数示例确实在内部使用了变量,但是该语言在后台进行了所有操作。您已经将不愉快的事情有效地委派给了您认为正确完成任务的人员。
Blrfl 2013年

12
@Blrfl:“幕后”争论终止了所有基于语言的争论,因为每一段代码最终都被翻译成x86机器代码。x86代码不是面向对象的,不是过程的,不是功能的,不是什么,但是这些类别对于编程语言来说是有价值的标签。看语言,而不是实现。
thiton

10
@thiton不赞成。Blrfl的意思是这些功能可能使用以相同编程语言编写的变量。无需在此处进行低级操作。该代码段仅使用库函数。您可以轻松地用Java编写相同的代码,请参见:(squaresOf(integers()).take(25)编写这些函数供读者练习;困难在于的无穷集integers(),但这对Java来说是个问题,因为它渴望评估,与Java无关变量)
Andres F.

6
那句话是令人困惑和误导的,那里没有魔术,只有语法糖
yannis 2013年

2
@thiton我建议您学习更多有关FP语言的知识,但是,代码片段不支持(或拒绝)不需要“变量”的断言(我假设您的意思是“可变变量”,因为其他种类在FP中很常见)。该代码段仅显示了也可以在Java中实现的库函数,除非存在懒惰/急切问题,否则此处将不作介绍。
Andres F.

Answers:


31

是否可以在不使用破坏性更新的情况下用Java实现这样的示例? 是。 但是,正如@Thiton和文章本身所提到的,这将是丑陋的(取决于个人喜好)。一种方法是使用递归。这是一个执行类似操作的Haskell示例:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

注意1)缺少突变,2)使用递归,3)缺少循环。最后一点非常重要-功能语言不需要在语言中内置循环结构,因为在Java中使用循环的大多数情况下,都可以使用递归。 这是一系列著名的论文,展示了如何令人难以置信的表达函数调用。


我发现这篇文章令人不满意,并想提出几点补充意见:

那篇文章对函数式编程及其好处是一个非常糟糕而令人困惑的解释。我强烈建议其他 来源来学习函数式编程。

关于本文最令人困惑的部分是,它没有提到Java(和大多数其他主流语言)的赋值语句有两种用法:

  1. 将值绑定到名称: final int MAX_SIZE = 100;

  2. 破坏性更新: int a = 3; a += 1; a++;

函数式编程避开了第二个,而包含了第一个(示例:let-expressions,函数参数,顶级defineition)。这是一个很重要的一点把握,否则文章似乎只是愚蠢的,可能让你想知道,是什么takesquares-of以及integers如果没有变数?

另外,该示例是没有意义的。它不显示的实现takesquares-ofintegers。就我们所知,它们是使用可变变量实现的。正如@Martin所说,您可以使用Java轻松编写此示例。

再一次,如果您真的想学习函数式编程,我建议避免使用本文和其他类似文章。它似乎更多是出于令人震惊和冒犯的目的,而不是讲授概念和基础知识。相反,为什么不查看约翰休斯写的我最喜欢的论文之一。休斯试图解决本文所涉及的一些相同问题(尽管休斯并未谈论并发/并行化);这是一个预告片:

本文试图向广大(非功能性)程序员群体展示功能性编程的重要性,并通过明确它们的优点来帮助功能性程序员充分利用其优势。

[...]

在本文的其余部分中,我们将论证功能语言提供了两种非常重要的新粘合剂。我们将给出一些程序示例,这些程序可以采用新的方式进行模块化,从而可以简化。这是函数式编程功能强大的关键-它允许改进的模块化。这也是功能程序员必须争取的目标-较小,更简单,更通用的模块,与我们将要描述的新胶粘在一起。


10
为“ +1”表示“如果您真的想学习函数式编程,我建议避免使用本文和其他类似文章。它似乎更多是出于令人震惊和冒犯的目的,而不是讲授概念和基础。”

3
人们不执行FP的原因一半是因为他们没有在uni上听到/学习任何东西,另一半则是因为当他们查看FP时,会发现一些文章使他们既不了解情况,又以为都是幻想玩而不是经过深思熟虑的有好处的方法。+1提供更好的信息来源
Jimmy Hoffa

3
如果您愿意,则将您对问题的答案放在绝对顶部,如果这样就可以更直接地回答该问题,那么也许该问题将保持开放状态(针对问题的直接答案)
Jimmy Hoffa 2013年

2
抱歉,nitpick,但我不明白您为什么选择此haskell代码。我读过LYAH,您的榜样让我难以理解。我也看不到与原始问题的关系。您为什么不仅仅take 25 (map (^2) [1..])作为示例?
Daniel Kaplan

2
@tieTYT好问题-感谢您指出这一点。我使用该示例的原因是因为它显示了如何使用递归和避免可变变量来生成数字列表。我的意图是让OP看到该代码并考虑如何在Java中执行类似的操作。要解决您的代码段,什么是[1..]?这是Haskell内置的一个很酷的功能,但没有说明生成此类列表的概念。我确定Enum该类的实例(语法要求)也很有用,但太懒了,找不到它们。因此,unfoldr。:)

27

你不会的 变量是命令式编程的核心,如果您尝试不使用变量进行命令式编程,那只会使每个人痛苦不已。在不同的编程范例中,样式是不同的,并且不同的概念构成了您的基础。Java中的变量在小范围内很好地使用时,没有什么坏处。要求不带变量的Java程序就像要求不带函数的Haskell程序,因此您不需要它,也不要让自己被视为劣质的命令式编程,因为它使用了变量。

因此,Java方式为:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

并且不要因为讨厌变量而愚弄自己以任何更复杂的方式编写它。


5
“讨厌变量”?糟糕...您对函数式编程有什么了解?您尝试了哪些语言?哪些教程?
Andres F.

8
@AndresF .:在Haskell进行了超过两年的课程。我不是说FP不好。但是,在许多FP-vs-IP讨论中(例如,链接的文章),都有一种谴责使用可重新分配的命名实体(AKA变量)的倾向,并且在没有充分理由或数据的情况下谴责这一趋势。我的书中不合理地谴责了仇恨。仇恨使代码变得非常糟糕。
thiton

10
“ 过多的变量”是因果关系的简化,因为en.wikipedia.org/wiki/Fallacy_of_the_single_cause即使在Java中,无状态编程也有很多好处,尽管我同意您的回答,即在Java中代价太高,复杂到该程序,并且是非惯用语。我仍然不会回避认为无状态编程好而有状态是不好的想法,因为有些情感反应而不是人们由于经验而得出的合理的,经过深思熟虑的立场。
Jimmy Hoffa

2
与@JimmyHoffa所说的一致,我将向您介绍John Carmack的有关命令式语言的函数式编程(在他的情况下为C ++)(altdevblogaday.com/2012/04/26/functional-programming-in-c)。
史蒂文·埃弗斯

5
不合理的谴责不是仇恨,避免易变的状态也不是不合理的。
Michael Shaw

21

我可以用递归执行的最简单的操作是具有一个参数的函数。它不是很像Java的风格,但是它确实起作用:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}

3
+1,用于尝试通过Java示例实际回答问题。
KChaloux

我会为高尔夫风格的演示文稿投票(请参阅Mod公告),但不能强迫自己按下箭头,因为此代码与我最喜欢的答案中的语句完全匹配:“ 1)缺少突变,2)使用递归,以及3)缺少循环”
gna

3
@gnat:这个答案是在Mod通知之前发布的。我不是追求伟大的风格,而是追求简单,并且满足了OP的原始问题。说明它可以用Java做这样的事情。
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner确定;这并不会阻止我使用DVing(因为您应该能够编辑以符合标准)-这是神奇的完美匹配。做得好,做得很好,很有教育意义-谢谢
2013年

16

在您的功能示例中,我们看不到squares-oftake功能的实现方式。我不是Java专家,但是我很确定我们可以编写这些函数来启用这样的语句...

squares_of(integers).take(25);

并没有太大的不同。


6
Nitpick: squares-of在Java中不是有效的名称(squares_of虽然)。但除此之外,好的一点表明,本文的示例很差。

我怀疑文章的integer惰性生成整数,并且该take函数squared-of从中选择25个数字integer。简而言之,您应该有一个integer函数可以延迟生成无穷大的整数。
OnesimusUnbound

调用(integer)函数之类的东西有点疯狂,函数仍然是将参数映射到值的东西。事实证明这(integer)不是一个函数,而仅仅是一个值。甚至可以说这integer是一个绑定到无限个数字的变量
Ingo

6

在Java中,您可以使用迭代器来执行此操作(尤其是无限列表部分)。在下面的代码示例中,提供给Take构造函数的数字可以任意大。

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

或使用可链接的工厂方法:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

在哪里SquaresOfTakeIntegers扩展Numbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}

1
这表明OO范例比功能范例具有优势。具有适当的OO设计,您可以模仿功能范式,但不能模仿功能样式的OO范式。
m3th0dman

3
@ m3th0dman:通过适当的OO设计,您可以半步模仿FP,就像任何具有字符串,列表和/或字典的语言都可以半步模仿OO。通用语言的图灵等效性意味着只要付出足够的努力,任何一种语言都可以模拟任何其他语言的功能。
cHao 2013年

注意,像in这样的Java风格的迭代器while (test.hasNext()) System.out.println(test.next())在FP中是不可以的。迭代器本质上是可变的。
cHao 2013年

1
我几乎不相信可以模仿真正的封装或多态性;同样,由于严格的评估,Java(在此示例中)无法真正模仿功能语言。我还相信可以以递归方式编写迭代器。
m3th0dman

@ m3th0dman:多态性一点也不难模仿。甚至C语言和汇编语言也可以做到。只需将方法设为对象中的字段或类描述符/ vtable。而且,从数据隐藏的角度来说,封装并不是严格要求的;一半的语言不提供它,当您的对象是不可变的时,人们是否可以看到它的胆量也无关紧要。所需要的只是数据包装,上述方法字段可以轻松提供。
cHao 2013年

6

简洁版本:

为了使单分配样式在Java中可靠地工作,您需要(1)某种不可变友好的基础结构,以及(2)消除尾调用的编译器或运行时级别支持。

我们可以编写许多基础结构,并且可以安排一些事情来避免填充堆栈。但是,只要每个调用占用一个堆栈帧,就可以进行多少次递归操作就受到限制。保持可迭代项较小和/或懒惰,并且不应有重大问题。至少您遇到的大多数问题都不需要立即返回一百万个结果。:)

还要注意,由于该程序实际上必须进行可见的更改才能运行,因此您不能使所有内容保持不变。但是,您可以仅在某些替代方案过于繁琐的某些关键点上使用一小部分必需的可变项(例如流),使自己的绝大多数内容保持不变。


长版:

简而言之,一个Java程序如果想做任何值得做的事情,就不能完全避免使用变量。 您可以包含它们,从而在很大程度上限制了可变性,但是语言和API的设计本身以及最终更改底层系统的需求,使得完全不变性是不可行的。

Java从一开始就被设计为命令式面向对象的语言。

  • 命令式语言几乎总是依赖某种可变变量。例如,相对于递归,它们倾向于迭代,几乎所有迭代结构-甚至while (true)for (;;)!-完全取决于某个变量在迭代之间不断变化。
  • 面向对象的语言几乎将每个程序都想象成对象之间相互发送消息的图形,并且在几乎所有情况下,它们都是通过变异某些东西来响应这些消息的。

这些设计决策的最终结果是,没有可变变量,Java无法更改任何状态-甚至就像打印“ Hello world!”一样简单。到屏幕的输出流包括将字节粘贴在可变缓冲区中。

因此,出于所有实际目的,我们仅限于从我们自己的代码中清除变量。好吧,我们可以做到这一点。几乎。基本上,我们需要用递归替换几乎所有迭代,并用递归调用返回所有更改,以返回更改后的值。像这样...

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

基本上,我们建立一个链接列表,其中每个节点本身就是一个列表。每个列表都有一个“头”(当前值)和一个“尾”(其余子列表)。大多数功能语言都做了类似的事情,因为它非常适合有效的不变性。“下一个”操作仅返回尾部,通常在递归调用堆栈中将其传递到下一级。

现在,这是这个东西的极其简化的版本。但这足以说明Java中这种方法的严重问题。考虑以下代码:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

尽管我们只需要25个整数就可以得到结果,squares_of但这并不知道。它会返回中的每个数字的平方integers。递归深度达2000万个级别会在Java中引起很大的问题。

瞧,您通常会像这样的功能性语言,具有一种称为“尾部消除”的功能。这就是说,当编译器看到代码的最后一个动作是调用自身(并在函数为非无效时返回结果)时,它将使用当前调用的堆栈框架而不是设置新的堆栈框架,而是执行“跳转” “调用”(因此使用的堆栈空间保持不变)。简而言之,它将尾递归转换为迭代的过程大约占90%。它可以处理十亿个整数,而不会溢出堆栈。(它最终仍然会用完内存,但是在32位系统上,组合十亿个整数的列表无论如何都会使您在内存方面陷入困境。)

在大多数情况下,Java不会这样做。(这取决于编译器和运行时,但是Oracle的实现并不这样做。)每次对递归函数的调用都会消耗堆栈帧的内存。用完太多,就会出现堆栈溢出。堆栈溢出几乎可以保证程序的终止。因此,我们必须确保不这样做。

一种半解决方案...懒惰的评估。我们仍然有堆栈限制,但是它们可以与我们有更多控制权的因素联系在一起。我们不必为了返回25而计算一百万个整数。:)

因此,让我们构建一些惰性评估基础结构。(这段代码经过了一段时间的测试,但是从那时起我已经对其进行了相当多的修改;请先读一下主意,而不是语法错误。:))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(请记住,如果这实际上在Java中是可行的,那么至少类似于上面的代码已经是API的一部分。)

现在,有了适当的基础结构,编写不需要可变变量且至少对于少量输入稳定的代码变得相当简单。

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

这通常可行,但是仍然容易出现堆栈溢出。尝试take输入20亿个整数并对其进行一些操作。:P最终将引发异常,至少直到64 GB以上的RAM成为标准。问题是,为程序堆栈保留的程序内存量不是很大。通常在1到8 MiB之间。(您可以要求更大,但是无论您要求多少,都没关系-您打电话给您take(1000000000, someInfiniteSequence),您得到一个例外。)幸运的是,通过懒惰的评估,薄弱环节是我们可以更好地控制的领域。我们只需要小心自己多少钱take()

由于我们的堆栈使用量呈线性增加,因此在扩展方面仍然存在很多问题。每个调用处理一个元素,然后将其余元素传递给另一个调用。不过,现在我考虑了一下,我们可以拉出一个技巧,这可能会给我们带来更多的发展空间:将通话链变成通话。考虑这样的事情:

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

workWith基本上将工作分为两半,并将每一半分配给另一个对自己的调用。由于每个调用将工作列表的大小减少一半,而不是一个,因此应按对数而不是线性地缩放。

问题是,此函数需要输入-对于链表,要获取长度,需要遍历整个列表。但是,这很容易解决。根本不在乎有多少个条目。:)上面的代码将与Integer.MAX_VALUEcount之类的东西一起工作,因为无论如何null都会停止处理。计数大部分在这里,所以我们有一个坚实的基础案例。如果您预计Integer.MAX_VALUE列表中会有更多条目,则可以检查workWith的返回值-结尾应为null。否则,请递归。

请记住,这涉及到您告诉它的尽可能多的元素。这不是偷懒;它立即执行其操作。您只想执行操作 -也就是说,仅用于将自身应用于列表中的每个元素的事。正如我现在正在考虑的那样,在我看来,如果保持线性,序列将不那么复杂;应该不会有什么问题,因为序列无论如何都不会调用它们自己-它们只是创建再次调用它们的对象。


3

我以前曾尝试用Java创建类似Lisp的语言的解释器(几年前,所有代码都像在sourceforge的CVS中一样丢失了),并且Java util迭代器对于函数式编程有点冗长在清单上。

这是基于序列接口的东西,该接口只需要两个操作即可获取当前值并从下一个元素开始获取序列。这些在方案中的功能之后被称为头和尾。

使用诸如Seqor Iterator接口之类的东西很重要,因为这意味着该列表是延迟创建的。该Iterator接口不能是不可变对象,所以不太适合函数式编程-如果你不能告诉你,如果进入一个函数值已经被改了,你失去的函数式编程的关键优势之一。

显然integers应该是所有整数的列表,所以我从零开始,交替返回正负。

有两种版本的正方形-一种创建自定义序列,另一种使用自定义序列map-Java 7没有lambda,因此我使用了一个接口-依次将其应用于序列中的每个元素。

该点square ( int x )功能仅去除需要调用head()两次-通常我会通过将值转换成最终的变量,但添加该功能是指有在程序中没有变量,只有函数的参数已经做到了这一点。

Java对于此类编程的冗长的工作使我用C99编写了第二版的解释器。

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}

3

如何在不使用可变变量的情况下编写有用的 Java程序。

从理论上讲,您可以只使用递归而不使用可变变量来实现Java中的几乎所有内容。

在实践中:

  • Java语言不是为此设计的。许多构建体都是为突变而设计的,没有它就很难使用。(例如,您不能初始化不可变长度的Java数组。)

  • 与图书馆同上。而且,如果您将自己限制在不使用变体的库类中,那就更难了。(您甚至不能使用String ...看一下如何hashcode实现。)

  • 主流Java实现不支持尾调用优化。这意味着算法的递归版本往往会“饿”堆栈空间。而且由于Java线程堆栈没有增长,因此您需要预先分配大堆栈...或risk StackOverflowError

将这三件事结合起来,对于编写没有可变变量的有用的(即非平凡的)程序,Java并不是一个切实可行的选择。

(但是,没关系。还有其他可用于JVM的编程语言,其中一些确实支持函数式编程。)


2

当我们在寻找概念的示例时,我想说让我们搁置Java,寻找一个不同但又熟悉的设置,以便在其中找到概念的熟悉版本。UNIX管道与链接惰性函数非常相似。

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

在Linux中,这意味着给我每个字节,每个字节由假位而不是真位组成,直到我食欲不振为止。将每个字节更改为换行符;编号由此创建的每一行;并生成该数字的平方。此外,我对25条线没有胃口。

我声称不建议程序员以这种方式编写Linux管道。这是相对正常的Linux Shell脚本。

我声称不建议程序员尝试用Java类似地编写相同的东西。原因是软件维护是软件项目生命周期成本的主要因素。我们不想通过呈现表面上是Java程序的东西来迷惑下一个程序员,但实际上是通过精心复制Java平台中已经存在的功能实际上以一种定制的一次性语言编写的。

另一方面,我声称,如果我们的某些“ Java”软件包实际上是用诸如Clojure和Scala之类的功能或对象/功能语言编写的Java虚拟机软件包,那么下一个程序员可能会更加接受。它们被设计为通过将函数链接在一起进行编码,并以Java方法调用的常规方式从Java进行调用。

再说一次,对于Java程序员来说,从地方的函数式编程中汲取灵感仍然是一个好主意。

最近,我最喜欢的技术是使用不可变的,未初始化的返回变量和单个出口,以便像某些功能语言编译器所做的那样,Java检查无论函数主体中发生了什么,我总是只提供一个返回值。例:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}


我大约有70%的人肯定Java已经进行了返回值检查。如果控件可能脱离非void函数的末尾,则应该收到有关“缺少返回语句”的错误。
cHao

我的观点:如果对它进行编码就int result = -n; if (n < 1) { result = 0 } return result;可以编译,并且编译器不知道您是否打算使其等同于我的示例中的函数。也许那个例子太简单了,以至于无法使该技术看起来有用,但是在一个具有很多分支的函数中,我觉得很清楚将结果分配一次,无论遵循什么路径。
minopret

if (n < 1) return 0; else return -n;但是,如果您说的话,那您就没问题了……而且更简单。:)在我看来,在这种情况下,“一次返回”规则实际上会导致不知道何时设置返回值的问题;否则,您可以只返回它,而Java将更有能力确定其他路径何时不返回值,因为您不再将值的计算与实际返回值区分开了。
cHao

或者,以您的答案为例if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;
cHao

我刚刚决定,我不想再为捍卫Java中的OnlyOneReturn规则花更多的时间。出去吧。当我想到Java编码实践时,如果想捍卫受功能编程实践影响的实践,我将替换该示例。在那之前,没有任何例子。
minopret

0

最简单的方法是将以下内容提供给Frege编译器,并查看生成的Java代码:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x

几天后,我发现自己的想法又回到了这个答案。毕竟,我的建议是在Scala中实现功能编程部分。如果我们考虑在真正想到Haskell的那些地方使用Scala(我认为我不是唯一的一个blog.zlemma.com/2013/02/20/…),我们是否至少应该考虑Frege?
minopret

@minopret这确实是Frege的小生境-认识并热爱Haskell却需要JVM的人们。我相信,有一天弗雷格(Frege)将足够成熟,至少可以认真考虑一下。
Ingo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.