私は読んでいた 関数型プログラミングについての記事 作家が述べているところ
(take 25 (squares-of (integers)))
変数がないことに注意してください。実際、3つの関数と1つの定数しかありません。 Javaで整数の2乗を変数を使用せずに書いてみてください。ああ、おそらくそれを行う方法はありますが、それは確かに自然なことではなく、私のようにうまく読めません。上記のプログラム。
これをJavaで実現することは可能ですか?最初の15の整数の二乗を出力する必要がある場合、変数を使用せずにforまたはwhileループを記述できますか?
Mod通知
この質問はコードゴルフコンテストではありません。さらに別のコードではなく、関連する概念を説明する回答(理想的には以前の回答を繰り返さない)を探しています。
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でループが使用されるほとんど(すべて?)の場合に再帰を使用できるため、言語に組み込まれたループ構造を必要としません。 ここにあります 非常に表現力豊かな関数呼び出しがどのように実行できるかを示すよく知られた一連の論文。
私はその記事に満足できないと感じたので、さらにいくつかの点を指摘したいと思います。
その記事は、関数型プログラミングとその利点について非常に貧弱で混乱を招く説明です。関数型プログラミングについて学ぶには、 othersources を強くお勧めします。
この記事で最も混乱する部分は、Java(および他のほとんどの主流言語)での代入ステートメントの2つの使用法があることについて言及していないことです。
値を名前にバインド:final int MAX_SIZE = 100;
破壊的な更新:int a = 3; a += 1; a++;
関数型プログラミングは2番目のものを避けますが、最初のものを包含します(例:let
- expressions、function parameters、top-level define
itions)。これは、把握しておくべき非常に重要なポイントです。それ以外の場合、記事はばかげているように見え、変数でない場合のtake
、squares-of
、integers
は何なのか疑問に思われるかもしれません。
さらに、この例は無意味です。 take
、squares-of
、またはintegers
の実装は表示されません。私たちが知っているすべてのことについて、それらは可変変数を使用して実装されています。 @Martinが言ったように、Javaでこの例を簡単に書くことができます。
繰り返しますが、関数型プログラミングについて本当に学びたいのであれば、この記事や他の記事は避けてください。概念や基本を教えるのではなく、衝撃的で気分を害することを目的として書かれているようです。代わりに、John Hughesによる 私のお気に入りの論文の1つ をチェックしてみませんか。ヒューズは、記事で取り上げたのと同じ問題のいくつかに取り組みます(ただし、ヒューズは並行性/並列化については触れていません)。ここにティーザーがあります:
このペーパーは、(非関数型)プログラマーのより大きなコミュニティに関数型プログラミングの重要性を実証するための試みであり、関数型プログラマーがその利点を十分に発揮できるようにして、それらの利点を明確にすることも目的としています。
[...]
この論文の残りの部分では、関数型言語が2つの新しい非常に重要な種類の接着剤を提供すると主張します。新しい方法でモジュール化でき、それによって簡略化できるプログラムの例をいくつか示します。これは、関数型プログラミングの能力の鍵です。これにより、モジュール化を改善できます。これは、関数型プログラマーが努力しなければならない目標でもあります。これから説明する新しい接着剤と一緒に接着された、より小さくてシンプルでより一般的なモジュールです。
あなたはしません。変数は命令型プログラミングの中心にあり、変数を使用せずに命令型プログラミングを行おうとすると、誰にでもお尻の痛みを引き起こしています。さまざまなプログラミングパラダイムでは、スタイルが異なり、さまざまな概念が基礎となります。 Javaの変数は、小さなスコープで適切に使用された場合、問題ありません。 Java変数のないプログラムを要求することは、関数のないHaskellプログラムを要求するようなものです。そのため、それを要求したり、命令型プログラミングを劣ったものとして見られたりすることはありません。変数を使用するためです。
したがって、Javaの方法は次のようになります。
for (int i = 1; i <= 25; ++i) {
System.out.println(i*i);
}
変数への憎しみのために、もっと複雑な方法でだまされてはいけません。
再帰でできる最も簡単なのは、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);
}
}
}
あなたの機能的な例では、squares-of
およびtake
関数が実装されています。私はJava=エキスパートではありませんが、これらの関数を記述してこのようなステートメントを有効にすることができると確信しています...
squares_of(integers).take(25);
それほど大きな違いはありません。
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());
}
}
ここで、SquaresOf
、Take
および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() {}
}
ショートバージョン:
Javaで単一割り当てスタイルを確実に機能させるためには、(1)不変に適したある種のインフラストラクチャ、および(2)末尾呼び出しの排除のためのコンパイラーレベルまたはランタイムレベルのサポートが必要です。
インフラストラクチャの多くを記述でき、スタックがいっぱいにならないように調整できます。ただし、各呼び出しがスタックフレームを取得する限り、実行できる再帰には制限があります。イテラブルを小さくしたり、遅延させたりしてください。大きな問題は発生しないはずです。少なくとも、発生する問題のほとんどは、一度に100万の結果を返す必要はありません。 :)
また、プログラムは実行に値するために目に見える変更を実際に行う必要があるため、everythingを不変にすることはできません。ただし、重要な可変要素(ストリームなど)の小さなサブセットを使用して、代替手段が煩わしすぎる特定の重要なポイントでのみ、独自のものの大部分を不変に保つことができます。
ロングバージョン:
簡単に言えば、A Javaプログラムは、何か価値のあることをしたい場合、変数を完全に回避することはできません。あなたはcontainそれら、したがって可変性を大幅に制限しますが、言語とAPIの設計そのものは、最終的には基礎となるシステムを変更する必要があるため、完全な不変性を実行不可能にします。
Javaは最初から命令、オブジェクト指向言語として設計されました。
while (true)
およびfor (;;)
! -反復から反復へとどこかで変化する変数に完全に依存しています。これらの設計決定の最終結果は、変更可能な変数がないと、Javaは何の状態も変更する方法がありません。 "Hello world!"を画面に出力するだけの簡単なものでも、 mutableバッファにバイトを固定することを含む出力ストリーム。
したがって、すべての実用的な目的のために、独自のコードから変数を追放することに制限されています。わかりました。ほとんど。基本的に必要なのは、ほとんどすべての反復を再帰に置き換え、すべての変更を変更された値を返す再帰呼び出しに置き換えることです。そのようです...
_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
のすべての数値の2乗を返します。 2,000万レベルの再帰は、Javaでかなり大きな問題を引き起こします。
通常、このような奇抜な関数型言語には、「末尾呼び出しの削除」と呼ばれる機能があります。つまり、コンパイラは、コードの最後の動作がそれ自体を呼び出す(そして関数がvoid以外の場合は結果を返す)と判断した場合、新しい呼び出しを設定する代わりに現在の呼び出しのスタックフレームを使用し、代わりに「ジャンプ」を実行します。 「呼び出し」の(使用されるスタック領域は一定のままです)。つまり、末尾再帰を反復に変える方法の約90%になります。スタックをオーバーフローさせることなく、これらの数十億の整数を処理できます。 (それでも最終的にはメモリが不足しますが、10億intのリストを集めると、32ビットシステムではとにかくメモリのように混乱します。)
ほとんどの場合、Javaはそれを行いません。 (コンパイラとランタイムに依存しますが、Oracleの実装はそれを行いません。)再帰関数を呼び出すたびに、スタックフレームに相当するメモリが消費されます。使いすぎると、スタックオーバーフローが発生します。スタックのオーバーフローは、プログラムの終了を保証します。ですから、そうしないように注意する必要があります。
1つの半回避策...遅延評価。スタックの制限はまだありますが、それらは私たちがより制御できる要因に関係している可能性があります。 25を返すためだけに100万の整数を計算する必要はありません。
それでは、遅延評価インフラストラクチャを構築しましょう。 (このコードはしばらく前にテストされましたが、それ以来かなり修正しました。構文エラーではなくアイデアを読んでください。)
_// 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);
}
_
これはほとんど機能しますが、スタックオーバーフローが発生する傾向があります。 20億整数をtake
ingして、それらに対して何らかのアクションを実行してみてください。 :P少なくとも64 = GBのRAMが標準になるまでは、最終的に例外をスローします。問題は、スタック用に予約されているプログラムのメモリの量がそれほど大きくないことです。通常は1から8 MiBの間です(もっと大きくすることもできますが、どれだけ求めるかは問題ではありません。take(1000000000, someInfiniteSequence)
を呼び出すと、になります。 例外が発生します。)幸い、遅延評価では、弱点はより適切に制御できる領域にあります。take()
の量に注意する必要があります。
スタックの使用量は直線的に増加するため、スケールアップには依然として多くの問題があります。各呼び出しは1つの要素を処理し、残りを別の呼び出しに渡します。しかし、私がそれについて考えたところで、私たちが引っ張ることができる1つのトリックがあります。それは、私たちにかなりの余地を得るかもしれません:呼び出しのチェーンをtreeの呼び出し。次のようなものを検討してください。
_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である必要があります。それ以外の場合は、再帰します。
覚えておいてください、これはあなたが言うように多くの要素に触れます。怠惰ではありません。すぐに機能します。 actions-つまり、リスト内のすべての要素にそれ自体を適用することを唯一の目的とするものに対してのみ、それを実行したいとします。私は今それを考え直しているので、線形に保たれる場合、シーケンスはそれほど複雑ではないように思えます。シーケンスはとにかく自分自身を呼び出さないので、問題にはなりません-シーケンスを再び呼び出すオブジェクトを作成するだけです。
私は以前、JavaでLISPのような言語のインタープリターを作成しようとしました(数年前、sourceforgeのCVSにあったため、すべてのコードが失われました)。また、Java utilイテレータは、リストの関数型プログラミングには少し冗長です。
これは、現在の値を取得し、次の要素から始まるシーケンスを取得するために必要な2つの操作を含む、シーケンスインターフェイスに基づくものです。これらは、schemeの関数にちなんで頭と尾と名付けられています。
リストが遅延して作成されることを意味するため、Seq
またはIterator
インターフェイスのようなものを使用することが重要です。 Iterator
インターフェースは不変オブジェクトにすることはできないため、関数型プログラミングにはあまり適していません。関数に渡す値が変更されたかどうかを判断できない場合、キーの1つを失います関数型プログラミングの利点。
明らかにintegers
はすべての整数のリストであるべきなので、ゼロから始めて、正と負の整数を交互に返しました。
正方形には2つのバージョンがあります。1つはカスタムシーケンスを作成し、もう1つはmap
を使用して「関数」を取得します-Java 7にはラムダがないため、インターフェイスを使用しました-そして、それを順番に各要素に適用します。
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() );
}
}
}
書き方便利 Java可変変数を使わないプログラム。
理論的には、Javaのほとんどすべてを、再帰のみを使用して変更可能な変数なしで実装できます。
実際には:
Java言語はこのために設計されていません。多くの構成は突然変異のために設計されており、それなしでは使用することは困難です(たとえば、可変長を初期化することはできませんJava変異のない配列。)
ライブラリの同上。そして、自分でミューテーションを使用しないライブラリクラスに限定すると、それはさらに困難になります。 (文字列を使用することもできません... hashcode
の実装方法を確認してください。)
メインストリームJavaの実装は、末尾呼び出しの最適化をサポートしていません。つまり、アルゴリズムの再帰バージョンは、スタックスペースが「空腹」になる傾向があります。そして、Java threadスタックは大きくなりません。大きなスタックを事前に割り当てる必要があります...またはリスクStackOverflowError
。
これら3つを組み合わせると、Javaは実際にはviableを書くためのオプションではありません便利 (つまり、重要な)可変変数のないプログラム。
(しかし、まあ、それは問題ありません。JVMで使用できる他のプログラミング言語がいくつかあり、それらのいくつかは関数型プログラミングをサポートしています。)
概念の例を探しているので、Javaを脇に置いて、慣れ親しんだバージョンの概念を見つけるための別の慣れ親しんだ設定を探します。UNIXパイプレイジー関数のチェーンにかなり似ています。
cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25
Linuxでは、これはつまり、食欲を失うまで、真のビットではなく偽のビットで構成されるバイトを私に与えます。それらの各バイトを改行文字に変更します。このようにして作成された各行に番号を付けます。その数の二乗を生成します。さらに、私には25行以上の食欲があります。
私は、プログラマーがLinuxパイプラインをそのように書くことは不当に勧められないだろうと主張します。これは比較的通常のLinuxシェルスクリプトです。
私はプログラマーがJavaで同じことを同じように書こうとするのは不当に勧められると主張します。その理由は、ソフトウェアのメンテナンスがソフトウェアプロジェクトのライフタイムコストの主要な要素であることです。表向きはJavaプログラムですが、実際には=に既に存在する機能を精巧に複製することにより、実際にはカスタムの1回限りの言語で書かれています。 Javaプラットフォーム。
一方、「Java」パッケージの一部が実際にJava関数型またはオブジェクト/関数型言語のいずれかで記述された仮想マシンパッケージである場合、次のプログラマーはより受け入れやすいと主張しますClojureおよびScalaとして。これらは、関数をチェーンすることによってコード化され、Javaメソッド呼び出しの通常の方法でJavaから呼び出されるように設計されています。
繰り返しになりますが、Javaプログラマーが関数型プログラミングからインスピレーションを得ることは、場所を選ばないことです。
最近、私のお気に入りのテクニックは、不変の初期化されていない戻り変数と単一の出口を使用して、一部の関数型言語コンパイラーが行うように、Javaが、関数、私は常に1つだけの戻り値を提供します。例:
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; }
アウトを見つける最も簡単な方法は、以下を Frege コンパイラにフィードし、生成されたJavaコードを確認することです。
module Main where
result = take 25 (map sqr [1..]) where sqr x = x*x