<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Fs2 on Adam J King</title><link>https://comforting-babka-f6ed67.netlify.app/tags/fs2/</link><description>Recent content in Fs2 on Adam J King</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 26 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://comforting-babka-f6ed67.netlify.app/tags/fs2/index.xml" rel="self" type="application/rss+xml"/><item><title>Pull up a seat with FS2 Streams</title><link>https://comforting-babka-f6ed67.netlify.app/p/pull-up-a-seat-with-fs2-streams/</link><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><guid>https://comforting-babka-f6ed67.netlify.app/p/pull-up-a-seat-with-fs2-streams/</guid><description>&lt;p&gt;I usually put &lt;a class="link" href="https://fs2.io/" target="_blank" rel="noopener"
&gt;FS2&lt;/a&gt; amongst my top favourite libraries in my career. This streaming library powers some
of the most popular Scala Typelevel libraries. I often encounter streaming design problems that make me think to
myself, &amp;ldquo;you can do this with a couple lines of FS2&amp;rdquo;. Enough gushing. I typically find that folks who come across FS2
struggle with creating a correct mental model for how a stream in FS2 works.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This article assumes you know about Cats-Effect and FS2 already and would like to dive deeper. Understanding how
FS2 works requires a mental model for how suspended side effects work too.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On the surface, an FS2 &lt;code&gt;Stream&lt;/code&gt; looks and feels a lot like a collection (like &lt;code&gt;List&lt;/code&gt;). It supports common collections
operations like &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, or &lt;code&gt;filter&lt;/code&gt;. In fact, when we go to complete a stream, we can convert our stream to a
host of different collection types.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scala" data-lang="scala"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;F&lt;/span&gt;, &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="o"&gt;???&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toList&lt;/span&gt; &lt;span class="c1"&gt;// F[List[Int]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toVector&lt;/span&gt; &lt;span class="c1"&gt;// F[Vector[Int]]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This property of &lt;code&gt;Stream&lt;/code&gt;s makes it easy to understand the API at first, but can make it difficult to understand how it
works internally. A &lt;code&gt;Stream&lt;/code&gt; differs from a &lt;code&gt;List&lt;/code&gt; in that it incorporates an effect type (&lt;code&gt;F[_]&lt;/code&gt;) as part of its
definition (&lt;code&gt;Stream[F, A]&lt;/code&gt;). So why do we need an effect type for our stream definition? A &lt;code&gt;Stream&lt;/code&gt; does not necesserily
hold all elements in memory at once. For example, if we stream an HTTP response, not all body bytes arrive from the
network at the same time.&lt;/p&gt;
&lt;p&gt;When using an effectful type (like &lt;code&gt;IO&lt;/code&gt;), a &lt;code&gt;Stream[IO, A]&lt;/code&gt; actually represents a single &lt;code&gt;IO[Option[A]]&lt;/code&gt; that we call
repeatedly for new results. We refer to this as a &amp;ldquo;pull-based&amp;rdquo; stream because we need to perform an action to retrieve
the next elements of the stream. This works the same as Scala&amp;rsquo;s &lt;code&gt;Iterator&lt;/code&gt; class when lazy generating results. As this
diagram shows, we don&amp;rsquo;t materialise the elements of the &lt;code&gt;Stream&lt;/code&gt; until we reach them.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://comforting-babka-f6ed67.netlify.app/p/pull-up-a-seat-with-fs2-streams/diagram.png"
width="474"
height="550"
srcset="https://comforting-babka-f6ed67.netlify.app/p/pull-up-a-seat-with-fs2-streams/diagram_hu_aea90a6c9034ba7.png 480w, https://comforting-babka-f6ed67.netlify.app/p/pull-up-a-seat-with-fs2-streams/diagram_hu_82442239ac43c8b5.png 1024w"
loading="lazy"
alt="A comparison of the mental models"
class="gallery-image"
data-flex-grow="86"
data-flex-basis="206px"
&gt;&lt;/p&gt;
&lt;p&gt;Another important thing to note about &lt;code&gt;Stream&lt;/code&gt;s; they don&amp;rsquo;t (always) contain single elements. Instead, they contain
multiple groups of elements (&lt;code&gt;Chunk&lt;/code&gt;s), which allows individual pulls to return more than one element. Useful when
working with things like response payloads where consuming bodies byte by byte could harm performance. At one time a
&lt;code&gt;Stream&lt;/code&gt; only holds a single chunk. It pulls a new chunk as necessary when we consume the stream.&lt;/p&gt;
&lt;h2 id="pull-api"&gt;Pull API
&lt;/h2&gt;&lt;p&gt;Armed with this understanding of how a &lt;code&gt;Stream&lt;/code&gt; works we can actually access the underlying representation type,
&lt;code&gt;Pull&lt;/code&gt;. Given an arbitrary stream we can call &lt;code&gt;.pull&lt;/code&gt; to convert a &lt;code&gt;Stream&lt;/code&gt; into a &lt;code&gt;Pull&lt;/code&gt;. This type covers some
internal implementation details but, in a nutshell, a &lt;code&gt;Pull&lt;/code&gt; monad represents a single &amp;ldquo;step&amp;rdquo; of the underlying
representation. For example, a single &lt;code&gt;Pull&lt;/code&gt; might represent polling a connection for new response bytes.&lt;/p&gt;
&lt;p&gt;Technically &lt;code&gt;.pull&lt;/code&gt; returns a &lt;code&gt;ToPull&lt;/code&gt; type. Again, this type covers some internal implementation details but acts as a
springboard for calls like &lt;code&gt;uncons&lt;/code&gt; or &lt;code&gt;peek&lt;/code&gt;, which can optionally consume the current chunk. Operations on this type
always yield a &lt;code&gt;Pull&lt;/code&gt; so for our purposes I will focus on that.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Pull&lt;/code&gt; type can cause confusion because it has three type parameters.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Pull[F, O, R]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▲ ▲ ▲
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │ └─────── Return Type
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └────────── Output Type
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └───────────── Effect Type
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The effect type simply represents the monad in use (e.g. &lt;code&gt;IO&lt;/code&gt;). The output type represents the type of the parent
&lt;code&gt;Stream&lt;/code&gt;. The return type represents a &amp;ldquo;working&amp;rdquo; type to allow use to compose multiple &lt;code&gt;Pull&lt;/code&gt;s into a single stream
step. We can only collapse &lt;code&gt;Pull&lt;/code&gt;s back to a normal &lt;code&gt;Stream&lt;/code&gt; if they return a &lt;code&gt;Unit&lt;/code&gt; type (signalling they are
&amp;ldquo;done&amp;rdquo;), using the &lt;code&gt;Pull.stream&lt;/code&gt; API. This distinction can make &lt;code&gt;Pull&lt;/code&gt; a confusing type to work with at first.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-scala" data-lang="scala"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Pull&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;, &lt;span class="kt"&gt;String&lt;/span&gt;, &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nc"&gt;Pull&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;_&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nc"&gt;Pull&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;, &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Pull&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;Pull.output1&lt;/code&gt; emits the given value as an element in our stream. &lt;code&gt;Pull.done&lt;/code&gt; just represents a &lt;code&gt;Pull[F, Nothing, Unit]&lt;/code&gt;
and acts as a convenient way of signalling the current pull has finished outputting any elements.&lt;/p&gt;
&lt;p&gt;You can define custom steps on an existing &lt;code&gt;Stream&lt;/code&gt; by using &lt;code&gt;.repeatPull&lt;/code&gt;. This gives you a high degree of control over
when and how we pull elements from the upstream. This function provides a base &lt;code&gt;ToPull&lt;/code&gt; and asks you to return an
&lt;code&gt;Option[Stream[F, A]]&lt;/code&gt; to indicate if and how the stream should continue. An interesting property of this stage is that
we don&amp;rsquo;t have to return elements from our upstream definition. We can insert new elements or switch to a different
stream entirely.&lt;/p&gt;
&lt;h2 id="motivation-and-summary"&gt;Motivation and Summary
&lt;/h2&gt;&lt;p&gt;FS2&amp;rsquo;s &lt;code&gt;Stream&lt;/code&gt; already provides a lot of powerful abstractions for working on a stream without dipping into the pull
API, so why might you want to?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performant consumption of chunks.&lt;/strong&gt; Individually stream operations can provide either full chunks (via &lt;code&gt;.chunks&lt;/code&gt;) or
single elements. If you ever want to combine elements across chunks or dynamically merge chunks then you will need to
pull and merge chunks yourself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Single step logic.&lt;/strong&gt; Sometimes you might want to partially consume a stream before making a decision on how to
process the full stream. The pull API allows you to arbitrarily consume the Stream without committing to a single
strategy.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Full control over execution.&lt;/strong&gt; You might have a sensitive upstream API where you want to carefully choose when
and how effects get executed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding how a &lt;code&gt;Stream&lt;/code&gt; works underneath can help you intuit what a stream can (and can&amp;rsquo;t) do, as well as
diagnosing any performance issues. The pull API unlocks performance improvements around chunk handling that the
typical &lt;code&gt;Stream&lt;/code&gt; interface might not provide. This might seem like a dense topic at first, but sometimes the best way
to learn something means using it. Then you should find the concepts click into place (and if they don&amp;rsquo;t, I
apologise unreservedly).&lt;/p&gt;</description></item></channel></rss>