web-dev-qa-db-ja.com

ストリームの遅延処理

次の問題シナリオがあります。

  • テキストファイルがあり、それを読み取って行に分割する必要があります。
  • 一部の行を削除する必要がある場合があります(固定されていない基準による)。
  • ドロップされない行は、いくつかの事前定義されたレコードに解析する必要があります。
  • 無効なレコードは削除する必要があります。
  • 重複するレコードが存在する可能性があり、そのような場合、それらは連続しています。重複または複数のレコードが存在する場合、1つのアイテムのみを保持する必要があります。
  • 残りのレコードは、1つのフィールドに含まれる値に従ってグループ化する必要があります。同じグループに属するすべてのレコードが次々に表示されます(例:AAAABBBBCCDEEEFFなど)。
  • 各グループのレコードには番号を付ける必要があります(1、2、3、4、...)。各グループの番号付けは1から始まります。
  • 次に、レコードをどこかに保存するか、作成されたのと同じ順序で消費する必要があります。

これをJavaまたはC++で実装する必要があります。

私の最初のアイデアは、次のような関数/メソッドを定義することでした:

  • ファイルからすべての行を取得する1つの方法。
  • 不要な行を除外する1つの方法。
  • フィルタリングされた行を有効なレコードに解析する1つの方法。
  • 重複するレコードを削除する1つの方法。
  • レコードをグループ化して番号を付ける1つの方法。

問題は、読み取るデータが大きすぎてメインメモリに収まらない可能性があることです。そのため、これらすべてのリストを作成して関数を順番に適用することはできません。

一方、レコードがすべての基になるデータ(基本的には前のレコードと現在のレコードの間のテキスト行、およびレコード)が消費されると、メインメモリにすべてのデータを一度に収める必要はないと思います。それ自体)は処分することができます。

Haskellについての知識がほとんどないので、ある種の遅延評価についてすぐに考えました。完全に計算されたリストに関数を適用する代わりに、互いに積み重ねられたさまざまなデータストリームがあります。毎回、各ストリームの必要な部分だけがメインメモリに実体化されます。

しかし、これをJavaまたはC++で実装する必要があります。したがって、私の質問は、これらの言語の1つでストリームのこの遅延処理を実装できるデザインパターンまたはその他の手法はどれかということです。

3
Giorgio

Javaを決定する場合は、イテレーターを調べる必要があります。

ファイルから1行を読み取るIterator<String>を記述します。コンストラクターで上記のイテレーターを受け入れるフィルタリングイテレーターを記述し、必要な行のみを生成します。コンストラクターで文字列イテレーターを受け入れ、各行をレコードに分割する分割Iterator<Record>を記述します。等々。

「もっとありますか?」で処理を行うことに気付くでしょう。ロジックを正しくするためのセクション。

6
user1249

この質問にはすでに受け入れられた答えがありますが、私は最近、別の興味深い解決策を見つけました。それをここで共有したいと思います。 Javaでストリームを実装するための他の手法について知りたいと思います。

私のソリューションの最初のアイデアは、Scalaのコースから生まれました。このコースでは、Scalaのライブラリストリーム(scala.collection.immutable.Stream)を試すことができました。このコースでは、Scalaのストリームは、tail()関数/メソッドが遅延計算されるリスト(consセルのシーケンス)と非常に似ていることを説明しました。tailシーケンスは、tail()がオンデマンドで計算されます。が初めて呼び出され、以降のアクセスのためにキャッシュされます。

もちろん、テールも遅延計算されます。つまり、tail()が呼び出されると、テールの最初のconsセルのみが生成されます。このストリームでtail()が呼び出されると、テールストリームの後続の要素が生成されます。 。

解決策の2番目の部分は この質問 とその答えの一部から来ました。私が必要とした唯一の拡張は、レイジーテールのコンスセルを実装することでした。

このソリューションに基づいて、通常の関数(メソッド)takedrop、および高階関数mapfilter、を定義するのは簡単でした。 dropWhiletakeWhileこれもストリームで遅延動作します。これらのツールを使用すると、文字列のストリームを操作するという元の問題をはるかに簡単に解決できます。

私の解決策

最初にprogrammersの質問に示されているリストを実装し、次に同様のデータ型Stream<T>を実装しました。コンスセルは、クラスCons<T>のオブジェクトで構成されます。

  1. タイプT(ヘッド)の値、および
  2. テールストリームを返す1つのapplyメソッドを持つクロージャ(関数オブジェクト)。

主な定義は次のとおりです。

// An interface to store functions (closures) that are
// used to produce a stream's tail on demand.
public interface IStreamFunction<A>
{
  public Stream<A> apply();
}

// Wrapper object that invokes a stream function
// and then stores the result for future invocations of tail().
public class Memoizer<Stream<A>>
{
  private final IStreamFunction<A> _f;

  private Stream<A> _v;

  public Memoizer(IStreamFunction<A> f)
  {
    _f = f;

    _v = null;
  }

  public Stream<A> value()
  {
    if (_v == null)
    {
      _v = _f.apply();
    }

    return _v;
  }
}

// Stream class with two subclasses implementing the empty stream
// and a cons cell, respectively.
public abstract class Stream<A>
{
  // Private constructor cannot be called by any subclasses except inner classes.
  private Stream()
  {
  }

  public abstract A head();

  public abstract Stream<A> tail();

  public abstract boolean isEmpty();

  // Empty stream.
  public static final class Nil<A> extends Stream<A>
  {
    public Nil()
    {
    }

    public A head()
    {
      throw new NoSuchElementException("Nil.head");
    }

    public Stream<A> tail()
    {
      throw new NoSuchElementException("Nil.tail");
    }

    public boolean isEmpty()
    {
      return true;
    }
  }

  // Cons cell.
  public static final class Cons<A> extends Stream<A>
  {
    private final A _h;
    private final Memoizer<Stream<A>> _m;

    public Cons(A h, IStreamFunction<A> f)
    {
      _h = h;
      _m = new Memoizer<Stream<A>>(f);
    }

    public A head()
    {
      return _h;
    }

    public Stream<A> tail()
    {
      return _m.value();
    }

    public boolean isEmpty()
    {
      return false;
    }
  }
}
2
Giorgio