All Downloads are FREE. Search and download functionalities are using the official Maven repository.

scalaz.stream.io.scala Maven / Gradle / Ivy

The newest version!
package scalaz.stream

import java.io._

import scalaz.concurrent.Task

import scodec.bits.ByteVector

import scala.annotation.tailrec
import scala.io.{Codec, Source}

import Process._

/**
 * Module of `Process` functions and combinators for file and network I/O.
 */
object io {

  // NB: methods are in alphabetical order

  /**
   * Like resource, but the `release` action may emit a final value,
   * useful for flushing any internal buffers. NB: In the event of an
   * error, this final value is ignored.
   */
  def bufferedResource[F[_],R,O](acquire: F[R])(
                            flushAndRelease: R => F[O])(
                            step: R => F[O]): Process[F,O] =
    await(acquire) { r =>
      repeatEval(step(r)).onComplete(eval(flushAndRelease(r)))
    }

  /**
   * Implementation of resource for channels where resource needs to be
   * flushed at the end of processing.
   */
  def bufferedChannel[R,I,O](acquire: Task[R])(
                             flush: R => Task[O])(
                             release: R => Task[Unit])(
                             step: R => Task[I => Task[O]]): Channel[Task,Option[I],O] = {
    resource(acquire)(release) {
      r =>
        val s = step(r)
        Task.now {
          case Some(i) => s flatMap (f => f(i))
          case None => flush(r)
        }
    }
  }

  /**
   * Creates a `Channel[Task,Int,ByteVector]` from an `InputStream` by
   * repeatedly requesting the given number of bytes. The last chunk
   * may be less than the requested size.
   *
   * This implementation requires an array allocation for each read.
   * To recycle the input buffer, use `unsafeChunkR`.
   *
   * This implementation closes the `InputStream` when finished
   * or in the event of an error.
   */
  def chunkR(is: => InputStream): Channel[Task,Int,ByteVector] =
    unsafeChunkR(is).map(f => (n: Int) => {
      val buf = new Array[Byte](n)
      f(buf).map(ByteVector.view)
    })

  /**
   * Creates a `Sink` from an `OutputStream`, which will be closed
   * when this `Process` is halted.
   */
  def chunkW(os: => OutputStream): Sink[Task,ByteVector] =
    resource(Task.delay(os))(os => Task.delay(os.close))(
      os => Task.now((bytes: ByteVector) => Task.delay(os.write(bytes.toArray))))

  /**
   * Creates a `Sink` from a file name and optional buffer size in bytes.
   *
   * @param append if true, then bytes will be written to the end of the file
   *               rather than the beginning
   */
  def fileChunkW(f: String, bufferSize: Int = 4096, append: Boolean = false): Sink[Task,ByteVector] =
    chunkW(new BufferedOutputStream(new FileOutputStream(f, append), bufferSize))

  /** Creates a `Channel` from a file name and optional buffer size in bytes. */
  def fileChunkR(f: String, bufferSize: Int = 4096): Channel[Task,Int,ByteVector] =
    chunkR(new BufferedInputStream(new FileInputStream(f), bufferSize))

  /** A `Sink` which, as a side effect, adds elements to the given `Buffer`. */
  def fillBuffer[A](buf: collection.mutable.Buffer[A]): Sink[Task,A] =
    channel.lift((a: A) => Task.delay { buf += a })

  /**
   * Creates a `Process[Task,String]` from the lines of a file, using
   * the `iteratorR` combinator to ensure the file is closed
   * when processing the stream of lines is finished.
   */
  def linesR(filename: String)(implicit codec: Codec): Process[Task,String] =
    linesR(Source.fromFile(filename)(codec))

  /**
   * Creates a `Process[Task,String]` from the lines of the `InputStream`,
   * using the `iteratorR` combinator to ensure the `InputStream` is closed
   * when processing the stream of lines is finished.
   */
  def linesR(in: => InputStream)(implicit codec: Codec): Process[Task,String] =
    linesR(Source.fromInputStream(in)(codec))

  /**
   * Creates a `Process[Task,String]` from the lines of the `Source`,
   * using the `iteratorR` combinator to ensure the `Source` is closed
   * when processing the stream of lines is finished.
   */
  def linesR(src: => Source): Process[Task,String] = {
    iteratorR(Task.delay(src))(src => Task.delay(src.close()))(r => Task.delay(r.getLines()))
  }

  /**
   * Creates `Sink` from an `PrintStream` using `f` to perform
   * specific side effects on that `PrintStream`.
   */
  def printStreamSink[O](out: PrintStream)(f: (PrintStream, O) => Unit): Sink[Task, O] =
    channel.lift((o: O) => Task.delay {
      f(out, o)
      if (out.checkError)
        throw Cause.Terminated(Cause.End)
    })

  /**
   * Turn a `PrintStream` into a `Sink`. This `Sink` does not
   * emit newlines after each element. For that, use `printLines`.
   */
  def print(out: PrintStream): Sink[Task,String] = printStreamSink(out)((ps, o) => ps.print(o))

  /**
   * Turn a `PrintStream` into a `Sink`. This `Sink` emits
   * newlines after each element. If this is not desired, use `print`.
   */
  def printLines(out: PrintStream): Sink[Task,String] = printStreamSink(out)((ps, o) => ps.println(o))

  /**
   * Generic combinator for producing a `Process[F,O]` from some
   * effectful `O` source. The source is tied to some resource,
   * `R` (like a file handle) that we want to ensure is released.
   * See `chunkW` for an example use.
   */
  def resource[F[_],R,O](acquire: F[R])(
                         release: R => F[Unit])(
                         step: R => F[O]): Process[F,O] =
    bracket(acquire)(r => eval_(release(r))){
      r => repeatEval(step(r))
    } onHalt { _.asHalt }

  /**
   * Create a Process from an iterator. The value behind the iterator should be
   * immutable and not rely on an external resource. If that is not the case, use
   * `io.iteratorR`.
   */
  def iterator[O](i: Task[Iterator[O]]): Process[Task, O] = {
    await(i) { iterator =>
      val hasNext = Task delay { iterator.hasNext }
      val next = Task delay { iterator.next() }

      def go: Process[Task, O] = await(hasNext) { hn => if (hn) eval(next) ++ go else halt }

      go
    }
  }

  /**
   * Create a Process from an iterator that is tied to some resource,
   * `R` (like a file handle) that we want to ensure is released.
   * See `linesR` for an example use.
   * @param req acquires the resource
   * @param release releases the resource
   * @param mkIterator creates the iterator from the resource
   * @tparam R is the resource
   * @tparam O is a value in the iterator
   * @return
   */
  def iteratorR[R, O](req: Task[R])(
                     release: R => Task[Unit])(
                     mkIterator: R => Task[Iterator[O]]): Process[Task, O] = {
    bracket[Task, R, O](req)(r => Process.eval_(release(r)))(r => iterator(mkIterator(r)) )
  }

  /**
   * The standard input stream, as `Process`. This `Process` repeatedly awaits
   * and emits chunks of bytes  from standard input.
   */
  def stdInBytes: Channel[Task, Int, ByteVector] =
    io.chunkR(System.in)

  /**
   * The standard input stream, as `Process`. This `Process` repeatedly awaits
   * and emits lines from standard input.
   */
  def stdInLines: Process[Task,String] =
    Process.repeatEval(Task delay { Option(scala.Console.readLine()) }) pipe process1.stripNone

  /**
   * The standard output stream, as a `Sink`. This `Sink` does not
   * emit newlines after each element. For that, use `stdOutLines`.
   */
  def stdOut: Sink[Task,String] =
    print(System.out)

  /**
   * The standard output stream, as a `ByteVector` `Sink`.
   */
  def stdOutBytes: Sink[Task, ByteVector] =
    chunkW(System.out)

  /**
   * The standard output stream, as a `Sink`. This `Sink` emits
   * newlines after each element. If this is not desired, use `stdOut`.
   */
  def stdOutLines: Sink[Task,String] =
    printLines(System.out)

  /**
   * Creates a `Channel[Task,Array[Byte],Array[Byte]]` from an `InputStream` by
   * repeatedly filling the input buffer. The last chunk may be less
   * than the requested size.
   *
   * It is safe to recycle the same buffer for consecutive reads
   * as long as whatever consumes this `Process` never stores the `Array[Byte]`
   * returned or pipes it to a combinator (like `buffer`) that does.
   * Use `chunkR` for a safe version of this combinator - this takes
   * an `Int` number of bytes to read and allocates a fresh `Array[Byte]`
   * for each read.
   *
   * This implementation closes the `InputStream` when finished
   * or in the event of an error.
   */
  def unsafeChunkR(is: => InputStream): Channel[Task,Array[Byte],Array[Byte]] =
    resource(Task.delay(is))(
             src => Task.delay(src.close)) { src =>
      Task.now { (buf: Array[Byte]) => Task.delay {
        val m = src.read(buf)
        if (m == buf.length) buf
        else if (m == -1) throw Cause.Terminated(Cause.End)
        else buf.take(m)
      }}
    }

  /**
   * Converts a source to a mutable `InputStream`.  The resulting input stream
   * should be reasonably efficient and supports early termination (i.e. all
   * finalizers associated with the input process will be run if the stream is
   * closed).
   */
  def toInputStream(p: Process[Task, ByteVector]): InputStream = new InputStream {
    import Cause.{EarlyCause, End, Kill}

    var cur = p

    var index = 0
    var chunks: Seq[ByteVector] = Nil    // we only consider the head to be valid at any point in time

    def read(): Int = {
      if (cur.isHalt && chunks.isEmpty) {
        -1
      } else {
        val buffer = new Array[Byte](1)
        val bytesRead = read(buffer)
        if (bytesRead == -1) {
          -1
        } else {
          buffer(0) & 0xff
        }
      }
    }

    override def read(buffer: Array[Byte], offset: Int, length: Int): Int = {
      if (cur.isHalt && chunks.isEmpty) {
        -1
      } else {
        // when our index walks off the end of our last chunk, we need to Nil it out!
        if (chunks.isEmpty) {
          step()
          read(buffer, offset, length)
        } else {
          @tailrec
          def go(offset: Int, length: Int, read: Int): Int = {
            if (chunks.isEmpty) {
              // we already took care of the "halted at start" stillborn case, so we can safely just step
              step()

              if (cur.isHalt && chunks.isEmpty)
                read         // whoops! we walked off the end of the stream and we're done
              else
                go(offset, length, read)
            } else {
              val chunk = chunks.head
              val remaining = chunk.length - index

              if (length <= remaining) {
                chunk.copyToArray(buffer, offset, index, length)

                if (length == remaining) {
                  index = 0
                  chunks = chunks.tail
                } else {
                  index += length
                }

                length + read
              } else {
                chunk.copyToArray(buffer, offset, index, remaining)

                chunks = chunks.tail
                go(offset + remaining, length - remaining, read + remaining)
              }
            }
          }

          go(offset, length, 0)
        }
      }
    }

    @tailrec
    override def close() {
      if (cur.isHalt && chunks.isEmpty) {
        chunks = Nil
      } else {
        cur = cur.kill
        cur.step match {
          case Halt(End | Kill) =>
            chunks = Nil

          case Halt(Cause.Error(e: IOException)) => throw e
          case Halt(Cause.Error(e: Exception)) => throw new IOException(e)

          // rethrow halting errors
          case Halt(Cause.Error(e)) => throw e

          case Step(Emit(_), cont) =>
            cur = cont.continue
            close()

          case Step(await: Await[Task,_,ByteVector], cont) => { // todo: ??? Cleanup
            // yay! run the Task
            cur = Util.Try(await.evaluate.run) +: cont
            close()
          }
        }
      }
    }

    @tailrec
    def step(): Unit = {
      index = 0
      cur.step match {
        case h @ Halt(End | Kill) =>
          cur = h

        case h @ Halt(Cause.Error(e: IOException)) => {
          cur = h
          throw e
        }

        case h @ Halt(Cause.Error(e: Exception)) => {
          cur = h
          throw new IOException(e)
        }

        // rethrow halting errors
        case h @ Halt(Cause.Error(e)) => {
          cur = h
          throw e
        }

        case Step(Emit(as), cont) => {
          chunks = as
          cur = cont.continue
        }

        case Step(Await(request, receive,_), cont) => { // todo: ??? Cleanup
          // yay! run the Task
          cur = Util.Try(receive(EarlyCause.fromTaskResult(request.attempt.run)).run) +: cont
          step()    // push things onto the stack and then step further (tail recursively)
        }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy