FS2

repository·main·Indexed 25 days ago

https://github.com/typelevel/fs2

A library for purely functional, effectful, and polymorphic stream processing in Scala, emphasizing resource safety and compositionality. It provides primitives for concurrent stream processing including Signal for values that change over time, Topic for broadcasting values with back-pressure, and Scan for stateful transformations of stream elements.

Tokens
23.4K
Snippets
38
Records
119
Agent score
81%

What's inside fs2

  1. What is FS2?

    main

    FS2 (Functional Streams for Scala) is a library for creating functional, effectful, and concurrent streams. It is designed to handle I/O computations (such as networking and file operations) in constant memory.

    Key capabilities include:

    • Constant memory I/O: Processing large data streams without exhausting memory.
    • Stateful transformations: Applying logic that depends on previous elements in the stream.
    • Resource safety: Ensuring effects and resources are evaluated and cleaned up correctly.
    • Ecosystem integration: Built on Cats Effect and serves as the streaming engine for libraries like http4s, skunk, and doobie.
  2. Streaming binary encoding and decoding with fs2-scodec

    main

    The fs2-scodec library enables streaming binary encoding and decoding by integrating scodec with FS2 streams. It provides two primary types for these operations:

    1. StreamDecoder: Used to decode binary data from a stream.
    2. StreamEncoder: Used to encode data into a binary stream.

    Both types can be constructed from standard scodec Decoder and Encoder values. Once an instance is created, it is typically converted into an FS2 Pipe using the .toPipeByte method to be used within a Stream pipeline.

    import cats.effect.{IO, IOApp}
    import scodec.bits._
    import scodec.codecs._
    import fs2.Stream
    import fs2.interop.scodec._
    import fs2.io.file.{Files, Path}
    
    object Decode extends IOApp.Simple {
    
      def run = {
        val frames: StreamDecoder[ByteVector] =
          StreamDecoder.many(int32).flatMap { numBytes => StreamDecoder.once(bytes(numBytes)) }
    
        val filePath = Path("largefile.bin")
    
        val s: Stream[IO, ByteVector] =
          Files[IO].readAll(filePath).through(frames.toPipeByte)
    
        s.compile.count.flatMap(cnt => IO.println(s"Read $cnt frames."))
      }
    }
  3. Explore the FS2 Ecosystem and Integrations

    main

    FS2 is widely used across various domains, including database drivers, messaging protocols, and cloud services. Below is a categorized list of notable libraries and integrations that leverage FS2.

    Data Stores and Databases

    • JDBC/SQL: doobie (pure functional JDBC), ldbc (JDBC for Cats Effect 3/Scala 3).
    • NoSQL/Document: mongo4cats (MongoDB), mongosaur (MongoDB driver), neotypes (Neo4j), redis4cats (Redis).
    • Search/Analytics: fs2-elastic (Elasticsearch).
    • Key/Value/Blob: fs2-blobstore (S3, GCS, SFTP), scarctic (Arctic).
    • Cassandra: fs2-cassandra.

    Messaging and Streaming Protocols

    • Kafka: kafka4s, fs2-kafka (fd4s), fs2-kafka (Spinoco).
    • RabbitMQ: cabbit, fs2-rabbit, Lepus.
    • MQTT: fs2-mqtt.
    • AWS Services: fs2-aws, sqs4s (SQS), pure-aws.
    • Other: fs2-google-pubsub (Google Cloud Pub/Sub), neutron (Apache Pulsar), fs2-jms (Java Messaging Service).

    Web and Network

    • HTTP: http4s (minimal/idiomatic HTTP), fs2-http (server/client), fs2-grpc (gRPC).
    • File Transfer: fs2-ftp (FTP/FTPS/SFTP), fs2-ssh (Apache SSHD wrapper).
    • DNS: vinyldns.

    Data Processing and Utilities

    • JSON: circe-fs2 (streaming JSON manipulation).
    • Binary/Encoding: scodec-stream (binary decoding/encoding), spata (CSV parser), fs2-data (various format parsers).
    • Security: fs2-aes (AES encryption), fs2-crypto (TLS support).
    • Control/Flow: fs2-throttler (token bucket throttling), upperbound (interval-based rate limiting), streamz (Akka Stream/Apache Camel integration).
  4. Core capabilities of FS2 streams

    main

    FS2 provides a declarative model for arbitrary control flow through several key capabilities:

    • Zipping and merging of streams: You can read from multiple sources simultaneously and combine their elements using zipping or merging combinators. The Stream algebra allows for highly flexible topologies.
    • Dynamic resource allocation: Streams can allocate resources dynamically (e.g., opening files discovered during the stream processing). FS2 guarantees these resources are released upon normal termination or when exceptions occur.
    • Nondeterministic and concurrent processing: Using concurrency combinators, you can build pipelines that read from multiple inputs simultaneously, handle nondeterminism, or implement queueing at various stages. This allows streams to act as lightweight, declarative threads for complex concurrent behavior.
  5. Migrate from Segment to Chunk based streams

    main

    In FS2 1.0, streams are internally represented by Chunks instead of Segments. This results in a simpler API and better performance for most use cases. All APIs that previously returned segments now return chunks.

    0.10 API1.0 API
    s.segmentss.chunks
    s.mapSegmentss.mapChunks
    s.scanSegmentss.scanChunks
    s.scanSegmentsOpts.scanChunksOpt
    s.pull.unconsChunks.pull.uncons
    Pull.outputChunkPull.output
  6. Convert a regular stream into a TimeSeries using timePulled

    main

    A regular stream of values might stop emitting if the source stops providing data, which can cause time-based aggregations to freeze. To prevent this, you can convert a regular stream into a "time series"—a stream of TimeStamped[Option[A]] where a None value represents a clock tick.

    Use TimeSeries.timePulled to timestamp each received value with the wall clock time it was pulled from the source. You must specify a tick period (e.g., 1.second) to ensure the stream continues to emit None values even when no input is present.

    import scala.concurrent.duration._
    import cats.effect.Temporal
    
    def withReceivedBitrate[F[_]: Temporal](input: Stream[F, Byte]): Stream[F, TimeStamped[Either[Long, Option[ByteVector]]]] =
      TimeSeries.timePulled(input.chunks.map(_.toByteVector), 1.second, 1.second).through(withBitrate)
  7. Use Topic for publish-subscribe patterns

    main

    Topic implements a general publish-subscribe pattern. It allows multiple subscribers to receive elements published to the topic. You can create a topic using Topic[F, A].flatMap (or Stream.eval(Topic[F, A])), publish elements using topic.publish (as a Pipe) or topic.publish1 (for a single element), and create subscribers using topic.subscribe(bufferSize).

    import cats.effect._
    import cats.effect.unsafe.implicits.global
    import fs2.Stream
    import fs2.concurrent.Topic
    
    Topic[IO, String].flatMap { topic =>
      val publisher = Stream.constant("1").covary[IO].through(topic.publish)
      val subscriber = topic.subscribe(10).take(4)
      subscriber.concurrently(publisher).compile.toVector
    }.unsafeRunSync()
  8. Use Signal or SignallingRef for stream communication

    main

    Signal (and its extension SignallingRef) can be used to communicate state between different streams, often to trigger interruptions or changes in behavior. SignallingRef[F, A] allows you to set a new value, which can then be used by other streams via interruptWhen(signal).

    import cats.effect._
    import cats.effect.unsafe.implicits.global
    import fs2.Stream
    import fs2.concurrent.SignallingRef
    
    import scala.concurrent.duration._
    
    SignallingRef[IO, Boolean](false).flatMap { signal =>
      val s1 = Stream.awakeEvery[IO](1.second).interruptWhen(signal)
      val s2 = Stream.sleep[IO](4.seconds) >> Stream.eval(signal.set(true))
      s1.concurrently(s2).compile.toVector
    }.unsafeRunSync()
  9. Use Channel for multiple publishers and a single subscriber

    main

    Channel implements a publish-subscribe pattern specifically useful when you have multiple publishers and a single subscriber. You can create an unbounded channel using Channel.unbounded[F, A] and interact with it via channel.send (to publish) and channel.stream (to subscribe).

    import cats.effect._
    import fs2.Stream
    import scala.concurrent.duration._
    import cats.effect.unsafe.implicits.global
    import fs2.concurrent.Channel
    
    Channel.unbounded[IO, String].flatMap { channel =>
      val pub1 = Stream.repeatEval(IO("Hello")).evalMap(channel.send).metered(1.second)
      val pub2 = Stream.repeatEval(IO("World")).evalMap(channel.send).metered(2.seconds)
      val sub = channel.stream.evalMap(IO.println)
      Stream(pub1, pub2, sub).parJoinUnbounded.interruptAfter(6.seconds).compile.drain
    }.unsafeRunSync()
  10. Use Pipes and Pipe2 for stream transformations

    main

    FS2 has simplified its transformation model by removing specific type aliases like Process1, Tee, Wye, and Channel. These are replaced by more general Pipe and Pipe2 types:

    • type Pipe[F,A,B] = Stream[F,A] => Stream[F,B]
    • type Pipe2[F,A,B,C] = (Stream[F,A], Stream[F,B]) => Stream[F,C]

    To apply these transformations to a stream, use the .through method for single-input pipes and .through2 for two-input pipes.

    Transformation Mapping:

    • Process1 functionality is now covered by Pipe.
    • Tee and Wye functionality is now covered by Pipe2.
    • Functions previously in the process1 module have moved to the pipe module.
    • tee and wye have been combined into the pipe2 module.
    // Before (0.8 or earlier)
    s.pipe(process1.take(10))
    s.wye(s2)(wye.blah)
    
    // After (0.9+)
    s.through(pipe.take(10))
    s.through2(s2)(pipe2.blah)
  11. Use Concurrent for cancelation instead of specialized async methods

    main

    FS2 1.0 leverages the cats.effect.Concurrent type class to handle cancelation. Many specialized methods in the old fs2.async package (like timedDequeue1, timedGet, or cancellableDequeue1) are no longer needed.

    Instead, you can compose cancelation using Concurrent[F].race or Concurrent[F].start. For example, to implement a timed get on a Deferred (formerly Promise), use p.get.timeout(duration).

  12. How GADTs are encoded in FS2

    main

    Because Scala lacks robust support for Generalized Algebraic Data Types (GADTs) and higher-rank types, FS2 uses a workaround: representing GADTs via their fold. Instead of using standard pattern matching (which fails with type lambdas or complex type parameters), you interact with these structures using an apply method that acts as a single-step pattern match.

    To consume a GADT-like structure in FS2, you provide two handlers to the apply method:

    1. An empty case handler: A function that is invoked if the structure is empty. For structures requiring type equality (e.g., A ~ B), this handler is provided with a 'proof' in the form of a pair of functions (A => B, B => A).
    2. A cons case handler: An implementation of a helper trait (often named H) that handles the non-empty case. This handler must be universal in the existential type parameter x introduced by the structure.