可変変数を使用せずに有用なJavaプログラムを作成する方法


12

私は読んでいた関数型プログラミングについての記事をどこライター状態

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

変数がないことに注意してください。実際、3つの関数と1つの定数しかありません。変数を使用せずにJavaで整数の2乗を書いてみてください。おそらく、それを行う方法はありますが、それは確かに自然なことではなく、上記の私のプログラムほど読みやすくはありません。

Javaでこれを達成することは可能ですか?最初の15個の整数の2乗を出力する必要がある場合、変数を使用せずにforループまたはwhileループを記述できますか?

Mod通知

この質問は、コードゴルフコンテストではありません。関係する概念を説明する(理想的には以前の回答を繰り返さずに)答えを探しています。さらに別のコードだけではありません。


19
あなたの機能の例ではありません内部で使用する変数を、しかし、言語は舞台裏で、それのすべてを行います。不快な部分を、正しくやったと思う人に効果的に委任しました。
Blrfl

12
@Blrfl:「舞台裏」引数は、すべてのコードが最終的にx86マシンコードに変換されるため、言語ベースの議論をすべて殺します。x86コードはオブジェクト指向でも手続き型でも機能的でも何でもありませんが、これらのカテゴリはプログラミング言語の貴重なタグです。実装ではなく言語を見てください。
チトン

10
@thiton同意しない。Blrflが言っているのは、これらの関数はおそらく同じプログラミング言語で書かれた変数を使用しているということです。ここで低レベルにする必要はありません。このスニペットは、単にライブラリ関数を使用しているだけです。同じコードをJavaで簡単に書くことができます:(squaresOf(integers()).take(25)これらの関数を書くことは読者の練習として残されています;困難はの無限のセットにありますが、integers()その熱心な評価のためにJavaの問題は何の関係もありません変数)
アンドレスF.

6
その引用は混乱を招き、誤解を招く恐れがあります。そこには魔法はありません。ただの構文糖です。
ヤニス

2
@hii kindはFPで一般的です)。このスニペットは、Javaにも実装されている可能性のあるライブラリ関数を示しているだけで、ここで話題になっていない遅延/熱心な問題はありません。
アンドレスF.

回答:


31

破壊的な更新を使用せずにJavaでそのような例を実装することは可能ですか? はい。 しかし、@ Thitonと記事自体が述べたように、それはいものです(自分の好みによって異なります)。1つの方法は、再帰を使用することです。同様のことを行う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(および他のほとんどの主流言語)の割り当てステートメントには2つの用途があるということを言及していないことです。

  1. 値を名前にバインドする: final int MAX_SIZE = 100;

  2. 破壊的な更新: int a = 3; a += 1; a++;

関数型プログラミングは2番目を回避しますが、1 番目を受け入れます(例:let-expressions、関数パラメーター、トップレベルのdefineイテオン)。それ以外の記事はただ愚かなようだと思ってあなたを残しかもしれませんが、どのようなので、これは、把握することが非常に重要なポイントであるtakesquares-ofintegersの変数でない場合は?

さらに、この例は無意味です。それはの実装を示していないtakesquares-ofまたはintegers。私たちが知る限り、それらは可変変数を使用して実装されています。@Martinが言ったように、この例をJavaで簡単に書くことができます。

繰り返しになりますが、関数型プログラミングについて本当に学びたいのであれば、この記事やその他の記事を避けることをお勧めします。コンセプトやファンダメンタルズを教えるのではなく、衝撃的で不快なことを目標に書かれているようです。代わりに、John Hughes の私のお気に入りの論文の1つをチェックしてみてください。Hughesは、記事で取り上げたのと同じ問題のいくつかに取り組んでいます(ただし、Hughesは並行性/並列化については言及していません)。ここにティーザーがあります:

このペーパーは、機能的プログラミングの重要性を(非機能的)プログラマーのより大きなコミュニティに示す試みであり、また、機能的プログラマーがその利点を明確にすることでその利点を最大限に活用するのを支援する試みです。

[...]

この論文の残りの部分では、関数型言語が2つの新しい非常に重要な種類の接着剤を提供すると主張します。新しい方法でモジュール化でき、それによって単純化できるプログラムの例をいくつか示します。これは、関数型プログラミングのパワーの鍵であり、モジュール化の改善を可能にします。また、機能的プログラマが努力しなければならない目標でもあります-小さくてシンプルで、より一般的なモジュールを、これから説明する新しい接着剤で接着します。


10
+1については、「関数型プログラミングについて本当に学びたいのであれば、この記事などを避けることをお勧めします。コンセプトやファンダメンタルズを教えるのではなく、衝撃的で不快なことを目標に書かれているようです。」

3
人々がFPを行わない理由の半分は、uniでそれについて何も聞いたり、学んだりしないためです。そして、残りの半分は、それを調べると、情報がなく、それがすべて空想にふさわしいと思う記事を見つけるためです考え抜かれた理由のあるアプローチではなく、利点を活用することです。より良い情報源を提供するための+1
ジミーホッファ

3
あなたはそれが質問へのより直接的だならば、絶対上部に質問にあなたの答えを入れて、多分この質問は(直接質問-焦点を当てて答えを)その後、開いたままになります
ジミー・ホッファ

2
nitpickに申し訳ありませんが、このhaskellコードを選んだ理由がわかりません。私はLYAHを読みましたが、あなたの例は私が理解するのが難しいです。また、元の質問との関係も見当たりません。なぜあなたtake 25 (map (^2) [1..])は例として使用しなかったのですか?
ダニエルカプラン

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
「変数の数」?Ooookay ...関数型プログラミングについて何を読みましたか?どの言語を試しましたか?どのチュートリアルですか?
アンドレス

8
@AndresF .: Haskellでの2年以上のコースワーク。FPが悪いとは言いません。ただし、多くのFP-vs-IPの議論(リンクされた記事など)では、再割り当て可能な名前付きエンティティ(別名変数)の使用を非難し、正当な理由やデータなしに非難する傾向があります。私の本では不当な非難が嫌われています。そして憎しみは本当に悪いコードになります。
チトン

10
「変数の憎しみ」は因果の過剰単純化ですen.wikipedia.org/wiki/Fallacy_of_the_single_caJavaではステートレスプログラミングにも多くの利点があるため、Javaではコストが高すぎて複雑になるというあなたの答えには同意しますがプログラムと非イディオマティックである。ステートレスプログラミングは良い経験であり、ステートフルは悪い経験であるという理性的でよく考えられたスタンスではなく感情的な反応であるという考えを、私はいまだに手放しません。
ジミーホッファ

2
@JimmyHoffaの発言に沿って、命令型言語での関数型プログラミング(彼の場合はC ++)のトピックについてJohn Carmackに紹介します(altdevblogaday.com/2012/04/26/functional-programming-in-c)。
スティーブンエバーズ

5
不当な非難は憎悪ではなく、可変状態を回避することは不合理ではありません。
マイケルショー

21

再帰でできる最も簡単な方法は、1つのパラメーターを持つ関数です。あまり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
Javaの例で実際に質問に答えようとする場合は+1。
-KChaloux

私はこれをdownvoteしたいコードのゴルフスタイルのプレゼンテーション(参照Modの通知を)しかし、このコードは完全に私の中で作られた文と一致するので、下矢印キーを押しに自己を強制することはできませんお気に入りの答え「1)変異の欠如、2)の使用再帰、および3)ループの欠如」
-gnat

3
@gnat:この回答はMod通知の前に投稿されました。私は素晴らしいスタイルを追求するつもりはありませんでした。シンプルさを追求し、OPの元々の質問を満足させました。Javaでそのようなことできることを説明するために。
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner確かに; これにより、DVingを止めることはできません(準拠するために編集できるはずなので)- 魔法をかけたのは驚くほど完璧なマッチです。よくやった、本当によくやった、かなり教育的-ありがとう
-gnat

16

関数の例では、squares-oftake関数がどのように実装されているかわかりません。私はJavaの専門家ではありませんが、このようなステートメントを有効にするためにこれらの関数を作成できると確信しています...

squares_of(integers).take(25);

それほど違いはありません。


6
Nitpick: squares-ofJavaでは有効な名前ではありません(squares_ofそうです)。しかし、そうでなければ、記事の例が貧弱であることを示す良い点。

記事のinteger遅延生成は整数であり、take関数はから25 squared-of個の数字を選択すると思われintegerます。つまり、integer無限に整数を遅延生成する関数が必要です。
OnesimusUnbound

(integer)関数のようなものを呼び出すのは少し狂気です-関数はまだ引数を値にマップするものです。これは(integer)関数ではなく、単なる値であることがわかります。今までのところ、それinteger変数の無限の範囲にバインドされている変数であると言うことさえできます。
インゴ

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());
    }
}

どこSquaresOfTakeおよびIntegers拡張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
これは、機能的パラダイムよりもオブジェクト指向パラダイムの優位性を示しています。適切なオブジェクト指向設計では、機能的なパラダイムを模倣できますが、機能的なスタイルでオブジェクト指向のパラダイムを模倣することはできません。
m3th0dman

3
@ m3th0dman:適切なオブジェクト指向設計を使用すると、文字列、リスト、および/または辞書を含む言語がオブジェクト指向を半ば模倣できるように、FPを半ば模倣することができます。汎用言語のチューリング等価性は、十分な労力を与えれば、どの言語でも他の言語の機能をシミュレートできることを意味します。
cHao

inのようなJavaスタイルのイテレータwhile (test.hasNext()) System.out.println(test.next())は、FPではno-noになることに注意してください。イテレータは本質的に可変です。
cHao

1
@cHao真のカプセル化やポリモーフィズムを模倣できるとは信じられません。また、厳密な熱心な評価のため、Java(この例では)は関数型言語を真に模倣できません。また、反復子は再帰的に作成できると考えています。
m3th0dman

@ m3th0dman:多態性を模倣するのは難しくありません。Cやアセンブリ言語でも可能です。メソッドをオブジェクトのフィールドまたはクラス記述子/ vtableにするだけです。また、データを隠すという意味でのカプセル化は厳密には必要ありません。そこにある言語の半分はそれを提供しません。あなたのオブジェクトが不変であるとき、人々がとにかくその内臓を見ることができるかどうかはそれほど重要ではありません。必要なのは、前述のメソッドフィールドで簡単に提供できるデータラッピングのみです。
cHao

6

短縮版:

Javaで単一割り当てスタイルを確実に機能させるには、(1)何らかの不変にフレンドリーなインフラストラクチャ、および(2)テールコールの除去に対するコンパイラレベルまたはランタイムレベルのサポートが必要です。

インフラストラクチャの多くを記述でき、スタックがいっぱいにならないように調整できます。ただし、各呼び出しがスタックフレームを使用する限り、実行できる再帰の量には制限があります。イテラブルを小さくしたり、怠laにしたりすると、大きな問題は発生しません。少なくとも、発生する問題のほとんどは、一度に100万件の結果を返す必要はありません。:)

また、プログラムは実行する価値があるために実際に目に見える変化をもたらさなければならないので、すべてを不変にすることはできません。ただし、必要な変更可能要素(ストリームなど)の小さなサブセットを使用して、代替物が面倒になる特定のキーポイントでのみ、自分のものの大部分を不変に保つことができます。


ロングバージョン:

簡単に言えば、Javaプログラムはやりがいのあることをしたい場合、変数を完全に回避することはできません。それら を含めることができるため、可変性を大幅に制限できますが、言語とAPIの設計自体が、最終的には基礎となるシステムを変更する必要があるため、完全な不変性を実現不可能にします。

Javaのは、次のように最初から設計されましたが不可欠オブジェクト指向言語。

  • 命令型言語は、ほぼ常に何らかの可変変数に依存しています。たとえば、再帰よりも反復を優先する傾向があり、ほぼすべての反復構成要素が偶数while (true)for (;;)!-反復ごとにどこかで変化する変数に完全に依存しています。
  • オブジェクト指向言語は、すべてのプログラムを相互にメッセージを送信するオブジェクトのグラフとして、そしてほとんどすべての場合、何かを変化させることによってそれらのメッセージに応答することを想定しています。

これらの設計上の決定の最終結果は、可変変数なしでは、Javaが何かの状態を変更する方法を持たないということです。画面への出力には、可変バッファへのバイトの固定を伴う出力ストリームが含まれます。

したがって、すべての実用的な目的のために、独自のコードから変数を削除することに制限されています。OK、私たちはちょっとそれをすることができます。ほとんど。基本的に必要なのは、ほとんどすべての反復を再帰で置き換え、すべての突然変異を変更された値を返す再帰呼び出しで置き換えることです。そのようです...

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()));
}

基本的に、各ノードがそれ自体のリストであるリンクリストを作成します。各リストには、「head」(現在の値)と「tail」(残りのサブリスト)があります。ほとんどの関数型言語はこれに似た何かをします。なぜなら、それは効率的な不変性に非常に敏感だからです。「次の」操作はテールを返すだけで、通常は再帰呼び出しのスタックの次のレベルに渡されます。

さて、これはこのものの非常に単純化されたバージョンです。しかし、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で非常に大きな問題を引き起こします。

参照してください、あなたが通常このような奇抜を行う関数型言語には、「テールコールの除去」と呼ばれる機能があります。つまり、コンパイラは、コードの最後の動作がそれ自体を呼び出す(および関数の非voidの場合は結果を返す)ことを確認すると、新しいものを設定する代わりに現在の呼び出しのスタックフレームを使用し、代わりに「ジャンプ」を実行します「呼び出し」の(したがって、使用されるスタック領域は一定のままです)。つまり、末尾再帰を反復に変換する方法の約90%になります。スタックをオーバーフローさせることなく、これらの10億の整数を処理できます。(最終的にはメモリ不足になりますが、32ビットシステムでは10億のintのリストを作成すると、メモリがめちゃくちゃになります。)

ほとんどの場合、Javaはそれを行いません。(コンパイラとランタイムに依存しますが、Oracleの実装はそれを行いません。)再帰関数の各呼び出しは、スタックフレームのメモリを消費します。使いすぎると、スタックオーバーフローが発生します。スタックのオーバーフローは、プログラムの死を保証します。ですから、そうしないように気をつけなければなりません。

1つの半回避策...遅延評価。スタックの制限はまだありますが、それらは、より詳細に制御できる要因に結び付けることができます。25を返すためだけに100万intを計算する必要はありません。

それでは、遅延評価インフラストラクチャを構築しましょう。(このコードはしばらく前にテストされましたが、それからかなり変更しました;構文エラーではなくアイデアを読んでください:))

// 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);
}

これは主に機能しますが、それでもスタックオーバーフローが発生しやすい傾向があります。take20億のintを試し、それらに対して何らかのアクションを実行します。:P少なくとも64 GB以上のRAMが標準になるまで、最終的に例外をスローします。問題は、スタック用に予約されているプログラムのメモリの量がそれほど大きくないことです。通常は1〜8 MiBです。(あなたは大きなを求めることができますが、それはすべての非常にどのくらいあなたが求めることは重要ではありません-あなたが呼び出すtake(1000000000, someInfiniteSequence)、あなたがします。例外を取得)の領域に私たちはより良いことができます幸い、遅延評価で、弱いスポットがあるコントロールを。私たちはどれだけ気をつけなければなりませんtake()

スタックの使用量は直線的に増加するため、スケールアップにはまだ多くの問題があります。各呼び出しは1つの要素を処理し、残りを別の呼び出しに渡します。しかし、今考えてみると、プルすることができる1つのトリックがあります。これにより、ヘッドルームをかなり増やすことができます:呼び出しのチェーンを呼び出しのツリーに変える。次のようなものを検討してください。

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基本的に作業を2つに分割し、それぞれの半分を別の呼び出しに割り当てます。呼び出しごとに作業リストのサイズが1つではなく半分に縮小されるため、これは線形ではなく対数的にスケーリングする必要があります。

問題は、この関数が入力を必要としていることです。リンクされたリストでは、長さを取得するにはリスト全体を走査する必要があります。ただし、これは簡単に解決できます。単に気にしないがいくつあるかのエントリ。:)上記のコードはInteger.MAX_VALUE、nullがとにかく処理を停止するため、カウントのようなもので動作します。カウントはほとんどそこにあるので、堅実なベースケースがあります。Integer.MAX_VALUEリストに複数のエントリがあると予想される場合は、workWithの戻り値を確認できます。最後はnullである必要があります。それ以外の場合、再帰。

覚えておいて、これはあなたがそれを伝える限り多くの要素に触れます。怠け者ではありません。それはすぐにそのことを行います。それはアクション、つまり、リスト内のすべての要素に自分自身を適用することを唯一の目的とするものに対してのみ行います。私は今考え直しているように、シーケンスが線形に保たれている場合、シーケンスはそれほど複雑ではないように思えます。シーケンスはとにかく自分自身を呼び出さないので、問題になるべきではありません-シーケンスを再度呼び出すオブジェクトを作成するだけです。


3

以前、JavaでLispライクな言語のインタープリターを作成しようとしました(数年前、sourceforgeのCVSにあったようにすべてのコードが失われました)。リストに。

これは、現在の値を取得し、次の要素からシーケンスを取得するために必要な2つの操作のみを備えたシーケンスインターフェイスに基づくものです。これらは、schemeの関数にちなんで、headとtailという名前が付けられています。

リストが遅延的に作成されることを意味するため、SeqまたはIteratorインターフェイスのようなものを使用することが重要です。Iteratorあなたが関数に渡す値は、それによって変更された場合、あなたが言うことができないならば、あなたは関数型プログラミングの重要な利点の1を失う-インターフェースは不変オブジェクトことはできませんので、あまり関数型プログラミングに適しています。

明らかintegersにすべての整数のリストである必要があるため、ゼロから始めて正と負の値を交互に返しました。

正方形には2つのバージョンがあります。1つはカスタムシーケンスを作成し、もう1つmapは「関数」を使用します。Java7にはラムダがないため、インターフェイスを使用し、シーケンスの各要素に順番に適用します。

square ( int x )関数のポイントは、head()2回呼び出す必要を取り除くことだけです-通常、値を最終変数に入れることでこれを行いますが、この関数を追加することは、プログラムに変数がなく、関数パラメータのみであることを意味します。

この種のプログラミングに対するJavaの冗長性により、代わりにC99でインタープリターの2番目のバージョンを作成することになりました。

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配列を突然変異なしで初期化することはできません。)

  • ライブラリについても同じです。そして、カバーの下で突然変異を使用しないライブラリクラスに自分自身を制限すると、さらに難しくなります。(文字列を使用することさえできません... hashcode実装方法を見てください。)

  • 主流のJava実装は、末尾呼び出しの最適化をサポートしていません。つまり、アルゴリズムの再帰バージョンはスタックスペースが「空腹」になる傾向があります。Javaスレッドのスタックが成長しないので、あなたは大きなスタック...またはリスクを事前に割り当てる必要がありますStackOverflowError

これらの3つのことを組み合わせると、Javaは可変変数なしで有用な(つまり重要な)プログラムを作成するための実行可能なオプションではありません。

(しかし、それは大丈夫です。JVMには他にもプログラミング言語があり、その一部は関数型プログラミングをサポートしています。)


2

概念の例を探しているので、Javaを別にして、概念の使い慣れたバージョンを見つけるための別の使い慣れた設定を探しましょう。UNIXパイプは、遅延関数のチェーンにかなり似ています。

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

Linuxでは、これは、食欲を失うまで、それぞれが本当のビットではなく偽のビットで構成されているバイトを私に与えます。これらの各バイトを改行文字に変更します。こうして作成された各行に番号を付けます。その数の二乗を生成します。さらに、私は25行以上の食欲を持っています。

私は、プログラマーがそのような方法でLinuxパイプラインを作成するのは悪くないだろうと主張しています。それは比較的普通のLinuxシェルスクリプトです。

私は、プログラマーがJavaで同じことを同じように書くことを試みるのは良くないだろうと主張します。その理由は、ソフトウェアプロジェクトのライフタイムコストにおける主要な要因としてのソフトウェアメンテナンスです。表面上はJavaプログラムであるが、Javaプラットフォームに既に存在する機能を精巧に複製することにより、実際にはカスタムの1回限りの言語で書かれているものを提示することで、次のプログラマを惑わせたくありません。

一方、「Java」パッケージの一部が、ClojureやScalaなどの機能言語またはオブジェクト/機能言語のいずれかで作成された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;
}


とにかく、Javaは既に戻り値のチェックをすでに行っていると確信しています。コントロールがvoid以外の関数の終わりから脱落する可能性がある場合、「returnステートメントの欠落」に関するエラーが発生するはずです。
cHao

私のポイント:int result = -n; if (n < 1) { result = 0 } return result;うまくコンパイルしてコードを作成し、コンパイラが私の例の関数と同等にするつもりかどうかわからない場合。たぶんその例は単純すぎてテクニックを役立たせることができないかもしれませんが、多くのブランチを持つ関数では、どのパスをたどっても結果が一度だけ割り当てられることを明確にした方がいいと思います。
minopret

と言うとif (n < 1) return 0; else return -n;、しかし、あなたは問題なく終わることになります...そしてそれはさらに簡単です。:)その場合、「1つの戻り値」ルールは、実際に戻り値がいつ設定されたかわからないという問題を引き起こすのに役立ちます。それ以外の場合は、それを返すだけで、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これは実際、Fregeが目標としているニッチです。Haskellを知って愛し、JVMを必要としている人々です。いつか、Fregeは少なくとも真剣に検討するのに十分な年齢になると確信しています。
インゴ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.