Skip to content

Commit 7b1cc07

Browse files
committed
.
1 parent 32dec17 commit 7b1cc07

File tree

1 file changed

+41
-20
lines changed

1 file changed

+41
-20
lines changed

content/unpack.md

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ and use the `RequestConfig` wrapper.
140140
Apart from the boilerplate, some things to note:
141141

142142
1. The `RequestConfig` object is really just an implementation detail of `download` meant
143-
to shared parameters and args between the different `download` methods. From a user
143+
to share parameters and args between the different `download` methods. From a user
144144
perspective, the name is meaningless and the contents are arbitrary: someone calling
145145
`downloadAsync` would have to pass some params inside a `RequestConfig`, some parameters
146146
outside `RequestConfig`, with no reason why some parameters should go in one place or another
@@ -149,14 +149,15 @@ Apart from the boilerplate, some things to note:
149149
objects that the user has to construct to call your method, possibly nested. The user would
150150
have to import several `Config` classes and instantiate a tree-shaped data structure just to
151151
call these methods. But this tree-structure does not model anything the user cares about, but
152-
instead models the code-sharing relationships between the various `def download` methods
152+
instead models the internal code-sharing relationships between the various `def download` methods
153153

154154
```scala
155155
case class RequestConfig(url: String,
156156
timeoutConfig: TimeoutConfig)
157157
case class TimeoutConfig(connectTimeout: Int,
158158
readTimeout: Int)
159159
case class AsyncConfig(retry: Boolean, ec: ExecutionContext)
160+
160161
def downloadSimple(config: RequestConfig) = doSomethingWith(config)
161162
def downloadAsync(config: RequestConfig, asyncConfig: AsyncConfig) = doSomethingWith(config)
162163
def downloadStream(config: RequestConfig, asyncConfig: AsyncConfig) = doSomethingWith(config)
@@ -176,19 +177,25 @@ val stream = downloadStream(
176177
)
177178
```
178179

179-
There are other more sophisticated ways that a library author can try to resolve this problem -
180+
Forcing the user to construct this tree-shaped `case class` data structure is an abstraction leak:
181+
the user has to write code matching the internal implementation details and code sharing of
182+
the `def download` methods, and construct the corresponding `case class` tree, even though they
183+
may really only care about calling a single `downloadAsync` method.
184+
185+
There are other more sophisticated ways that a library author can try to mitigate this -
180186
e.g. builder patterns - but the fundamental problem is unsolvable today. `unpack`/`*` solves
181187
this neatly, allowing the library author to use `unpack` in their definition-site parameter lists
182188
to share parameters between definitions, and the library user can either pass parameters
183189
individually or unpack a configuration object via `*`, resulting in both the definition site
184-
and the call site being boilerplate-free, even in the more involved example above:
190+
and the call site being boilerplate-free even in the more involved example below:
185191

186192
```scala
187193
case class RequestConfig(url: String,
188194
unpack timeoutConfig: TimeoutConfig)
189195
case class TimeoutConfig(connectTimeout: Int,
190196
readTimeout: Int)
191197
case class AsyncConfig(retry: Boolean, ec: ExecutionContext)
198+
192199
def downloadSimple(unpack config: RequestConfig) = doSomethingWith(config)
193200
def downloadAsync(unpack config: RequestConfig, unpack asyncConfig: AsyncConfig) = doSomethingWith(config)
194201
def downloadStream(unpack config: RequestConfig, unpack asyncConfig: AsyncConfig) = doSomethingWith(config)
@@ -279,6 +286,7 @@ class Requester{
279286
)
280287
...
281288
}
289+
282290
def stream(
283291
url: String,
284292
auth: RequestAuth = sess.auth,
@@ -399,6 +407,7 @@ class Requester{
399407
)
400408
...
401409
}
410+
402411
def stream(
403412
unpack request: Request,
404413
chunkedUpload: Boolean = false,
@@ -519,7 +528,9 @@ os.walk.attrs(path, preOrder = false, followLinks = true)
519528
os.walk.stream(path, preOrder = false, followLinks = true)
520529
```
521530

522-
These are defined as
531+
These are defined as shown below: each version of `os.walk` has a different return type, and
532+
so needs to be a different method, but they share many parameters and default values, and
533+
require a lot of boilerplate forwarding these internally:
523534

524535
```scala
525536
object walk{
@@ -540,6 +551,7 @@ object walk{
540551
includeTarget
541552
).toArray[Path].toIndexedSeq
542553
}
554+
543555
def attrs(
544556
path: Path,
545557
skip: (Path, os.StatInfo) => Boolean = (_, _) => false,
@@ -559,6 +571,7 @@ object walk{
559571
)
560572
.toArray[(Path, os.StatInfo)].toIndexedSeq
561573
}
574+
562575
object stream {
563576
def apply(
564577
path: Path,
@@ -577,6 +590,7 @@ object walk{
577590
includeTarget
578591
).map(_._1)
579592
}
593+
580594
def attrs(
581595
path: Path,
582596
skip: (Path, os.StatInfo) => Boolean = (_, _) => false,
@@ -593,37 +607,44 @@ With `unpack`, this could be consolidated into
593607

594608
```scala
595609
object walk{
596-
case class Config(path: Path,
597-
skip: Path => Boolean = _ => false,
598-
preOrder: Boolean = true,
599-
followLinks: Boolean = false,
600-
maxDepth: Int = Int.MaxValue,
601-
includeTarget: Boolean = false)
602-
603-
def apply(unpack config: Config): IndexedSeq[Path] = {
610+
case class Config[SkipType](path: Path,
611+
skip: SkipType = _ => false,
612+
preOrder: Boolean = true,
613+
followLinks: Boolean = false,
614+
maxDepth: Int = Int.MaxValue,
615+
includeTarget: Boolean = false)
616+
617+
def apply(unpack config: Config[os.Path => Boolean]): IndexedSeq[Path] = {
604618
stream(config*).toArray[Path].toIndexedSeq
605619
}
606-
def attrs(unpack config: Config): IndexedSeq[(Path, os.StatInfo)] = {
620+
def attrs(unpack config: Config[(os.Path, os.StatInfo) => Boolean]): IndexedSeq[(Path, os.StatInfo)] = {
607621
stream.attrs(config*)
608622
.toArray[(Path, os.StatInfo)].toIndexedSeq
609623
}
610624
object stream {
611-
def apply(unpack config: Config): Generator[Path] = {
625+
def apply(unpack config: Config[os.Path => Boolean]): Generator[Path] = {
612626
attrs(path, (p, _) => skip(p), preOrder, followLinks, maxDepth, includeTarget).map(_._1)
613627
}
614-
def attrs(unpack config: Config): Generator[(Path, os.StatInfo)] = ???
628+
def attrs(unpack config: Config[(os.Path, os.StatInfo) => Boolean]): Generator[(Path, os.StatInfo)] = ???
615629
}
616630
}
617631
```
618632

619633
Things to note:
620634

621-
1. A lot of these methods are forwarders/wrappers for each other, purely for convenience, and
635+
1. The different `def`s can all share the same `unpack config: Config` parameter to share
636+
the common parameters
637+
638+
2. The `.attrs` method take a `Config[(os.Path, os.StatInfo) => Boolean]`, while the
639+
`.apply` methods take a `Config[os.Path => Boolean]`, as the shared parameters have some
640+
subtle differences accounted for by the type parameter
641+
642+
3. A lot of these methods are forwarders/wrappers for each other, purely for convenience, and
622643
`*` can be used to forward the `config` object from the wrapper to the inner method
623644

624-
2. Sometimes the parameter lists are subtly different, e.g. `walk.stream.apply` and
625-
`walk.stream.attrs` have a different type for `skip`. In such cases `unpack` cannot work
626-
and so the forwarding has to be done manually.
645+
4. Sometimes the parameter lists are subtly different, e.g. `walk.stream.apply` and
646+
`walk.stream.attrs` have a different type for `skip`. In such cases `*` at the call-site
647+
cannot work and so the forwarding has to be done manually.
627648

628649
## Detailed Behavior
629650

0 commit comments

Comments
 (0)