Skip to content

Commit 74f97c2

Browse files
Stephan SahmStephan Sahm
authored andcommitted
adapted and created doc
1 parent 7261cb0 commit 74f97c2

2 files changed

Lines changed: 255 additions & 19 deletions

File tree

docs/src/manual.md

Lines changed: 248 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,82 @@
11
Manual
22
======
33

4-
This package defines julia implementations for the common types `Option` (aka `Maybe`), `Either` and `Try`, as well as one extra type `ContextManager` which mimics Python's `with`-ContextManager.
4+
This package defines julia implementations for the common types `Option` (aka `Maybe`), `Either` and `Try`, as well as
5+
one extra type `ContextManager` which mimics Python's `with`-ContextManager.
56

7+
Unlike typical implementations of `Option`, and `Either` which define them as new separate type-hierarchies, in
8+
`DataTypesBasic` both actually share a common part: `Identity`.
69

7-
## Option
810

9-
`Option{T} = Union{Identity{T}, Nothing}`
11+
Identity
12+
--------
13+
14+
`Identity` is the most basic container you can imagine. It just contains one value and always one value. It is similar
15+
to `Base.Some`, however with one notable difference. While `Some([]) != Some([])` (because it is treated more
16+
like `Ref`), for `Identity` we have `Identity([]) == Identity([])`, as `Identity` works like a Container.
17+
18+
```jldoctest
19+
julia> a = Identity(3)
20+
Identity(3)
21+
julia> map(x -> 2x, a)
22+
Identity(6)
23+
julia> foreach(println, a)
24+
3
25+
julia> for i in Identity("hello")
26+
print("$i world")
27+
end
28+
hello world
29+
```
30+
31+
Think of `Identity` as lifting a value into the world of containers. `DataTypesBasic` knows a lot about how to convert
32+
an `Identity` container to a `Vector` or else. This will come in handy soon. For now it is important to understand
33+
that wrapping some value `42` into `Identity(42)` makes it interactable on a container-level.
34+
35+
Nothing
36+
-------
37+
38+
`DataTypesBasic` makes heavy use of the builtin `Base.Nothing`. In the context here, `nothing` is interpreted as an
39+
empty container, something without any element. Or from another perspective: `nothing` works like a short-cycling abort.
40+
41+
42+
Const
43+
-----
44+
45+
`Const` looks like `Identity`, but behaves like `Nothing`. Its name suggests that whatever is in it will stay
46+
**const**ant. It behaves like an empty container, and can also be seen as aborting a program. Compared to `Nothing`,
47+
the `Const` container not only aborts, but further returns some useful abort-info.
48+
49+
```julia
50+
struct Identity{T}
51+
value::T
52+
end
53+
struct Const{T}
54+
value::T
55+
end
56+
```
57+
58+
```jldoctest
59+
julia> a = Const(3)
60+
Const(3)
61+
julia> map(x -> 2x, a)
62+
Const(3)
63+
julia> foreach(println, a)
64+
65+
julia> for i in Identity("hello")
66+
print("$i world")
67+
end
68+
69+
```
70+
71+
*******
72+
73+
Option
74+
-------
75+
76+
Option is a container which has either 1 value or 0. Having `Identity` and `Nothing` already at hand, it is defined as
77+
```julia
78+
Option{T} = Union{Identity{T}, Nothing}
79+
```
1080

1181
Use it like
1282
```julia
@@ -20,9 +90,11 @@ fo(Option(nothing)) # "fallback behaviour"
2090
fo(Option()) # "fallback behaviour"
2191
```
2292

23-
The real power of `Option` comes from generic functionalities which you can define on it. `DataTypesBasic` already defines the following:
24-
`Base.iterate`, `Base.foreach`, `Base.map`, `Base.get`, `DataTypesBasic.iftrue`, `DataTypesBasic.iffalse`, `Base.isnothing`, `DataTypesBasic.issomething`. Please
25-
consult the respective function definition for details.
93+
The real power of `Option` comes from generic functionalities which you can define on it. `DataTypesBasic` already
94+
defines the following:
95+
`Base.iterate`, `Base.foreach`, `Base.map`, `Base.get`, `Base.Iterators.flatten`, `DataTypesBasic.iftrue`,
96+
`DataTypesBasic.iffalse`, `Base.isnothing`, `DataTypesBasic.issomething`.
97+
Please consult the respective function definition for details.
2698

2799
Here an example for such a higher level perspective
28100
```julia
@@ -31,7 +103,8 @@ using DataTypesBasic
31103
flatten(a::Identity) = a.value
32104
flatten(a::Nothing) = a
33105

34-
function map2(f, a::Option{S}, b::Option{T}) where {S, T} # this comes
106+
# map a function over 2 Options, getting an option back
107+
function map2(f, a::Option{S}, b::Option{T}) where {S, T}
35108
nested_option = map(a) do a′
36109
map(b) do b′
37110
f(a′, b′)
@@ -52,21 +125,180 @@ end # nothing
52125
The package `TypeClasses.jl` (soon to come) implements a couple of such higher level concepts of immense use
53126
(like Functors, Applicatives and Monads).
54127

128+
For further details, don't hesitate to consult the source code `src/Option.jl` or take a look at the tests
129+
`test/Option.jl`.
130+
131+
132+
Either
133+
------
134+
135+
`Either` is exactly like `Option ` a container which has either 1 value or 0. In addition to `Option`, the `Either`
136+
data type captures extra information about the empty case.
137+
138+
As such it is defined as a union of the two types `Identity` and `Const`.
139+
```julia
140+
Either{Left, Right} = Union{Const{Left}, Identity{Right}}
141+
```
142+
143+
It is typical to describe the `Const` part as "Left" in the double meaning of being the left type-parameter as well as
144+
a hint about its semantics of describing the empty case, like "what is finally left". On the other hand "Right" also has
145+
its double meaning of being the right type-parameter, and also the "the right value" in the sense of correct (no abort).
146+
147+
148+
Use it like
149+
```julia
150+
using DataTypesBasic
151+
152+
fe(a::Identity{Int}) = a.value * a.value
153+
fe(a::Const{String}) = "fallback behaviour '$(a.value)'"
154+
155+
fe(Either{String}(7)) # 49
156+
fe(Either{String}("some error occured")) # "fallback behaviour 'some error occured'"
157+
158+
myeither = either("error", 2 > 3, 42)
159+
fe(myeither) # "fallback behaviour 'error'"
160+
```
161+
162+
You also have support for `Iterators.flatten` in order to work "withing" Either, and combine everything correctly.
163+
```julia
164+
check_threshold(a) = a < 15 ? Const((a, "threshold not reached")) : Identity("checked threshold successfully")
165+
166+
map(check_threshold, Identity(30)) |> Iterators.flatten # Identity("checked threshold successfully")
167+
map(check_threshold, Identity(12)) |> Iterators.flatten # Const((12, "threshold not reached"))
168+
# when working within another Const, think of it as if the first abort always "wins"
169+
map(check_threshold, Const("something different already happend")) |> Iterators.flatten # Const("something different already happend")
170+
```
171+
172+
Similar like for `Option`, there are many higher-level concepts (like Functors, Applicatives and Monads) which power
173+
unfold also over `Either`. Checkout the package `TypeClasses.jl` (soon to come) for a collection of standard helpers
174+
and interfaces.
175+
176+
For further details, don't hesitate to consult the source code `src/Either.jl` or take a look at the tests
177+
`test/Either.jl`.
178+
179+
180+
Try
181+
----
182+
183+
`Try` is a special case of `Either`, where `Const` can only bear Exceptions.
184+
```julia
185+
Try{T} = Union{Const{<:Exception}, Identity{T}}
186+
```
187+
188+
This is very handy. It gives you the possibility to work with errors just as you would do with other values, no need for
189+
dealing with `try`-`catch`.
190+
191+
Use it like
192+
```julia
193+
using DataTypesBasic
194+
195+
ft(a::Identity) = a.value * a.value
196+
ft(a::Const{<:Exception}) = "got an error '$(a.value)'"
197+
198+
ft(@Try 1/0) # Inf
199+
ft(@TryCatch ErrorException error("some error")) # "got an error 'Thrown(ErrorException(\"some error\"))'"
200+
201+
ft(@TryCatch ArgumentError error("another error")) # raises the error as normal
202+
```
203+
204+
There are many higher-level concepts (like Functors, Applicatives and Monads) which power also applies to `Try`.
205+
Checkout the package `TypeClasses.jl` (soon to come) for a collection of standard helpers and interfaces.
55206

56-
## [TODO] Try
207+
For further details, don't hesitate to consult the source code `src/Try.jl` or take a look at the tests
208+
`test/Try.jl`.
57209

58-
Please see the tests `test/Try.jl`.
59210

60-
## [TODO] Either
211+
*************
61212

62-
Please see the tests `test/Either.jl`.
213+
ContextManager
214+
--------------
63215

64-
## [TODO] ContextManager
216+
Finally there is the context-manager. It is quite separate from the others, however still one of my major containers
217+
which I used a lot in my passed in other programming languages. For instance in Python context-managers have the extra
218+
`with` syntax, which allows you to wrap code blocks very simply with some Initialization & Cleanup, handled by the
219+
context-manager.
65220

66-
Please see the tests `test/ContextManager.jl`.
221+
The way we represent a contextmanager is actually very compact.
222+
"""
223+
function which expects one argument, which itself is a function. Think of it like the following:
224+
```julia
225+
struct ContextManager{Func}
226+
f::Func
227+
end
228+
```
229+
It just takes a function. However the function needs to follow some rules
230+
```julia
231+
function contextmanagerready_func(cont) # take only a single argument, the continuation function `cont`
232+
# ... do something before
233+
value = ... # create some value to work on later
234+
result = cont(value) # pass the value to the continuation function (think like `yield`, but call exactly once)
235+
# ... do something before exiting, e.g. cleaning up
236+
result # IMPORTANT: always return the result of the `cont` function
237+
end
238+
```
239+
Now you can wrap it into `ContextManager(contextmanagerready)` and you can use all the context manager
240+
functionalities right away.
241+
242+
Let's create some ContextManagers
243+
```julia
244+
using DataTypesBasic
245+
context_print(value) = ContextManager(function(cont)
246+
println("initializing value=$value")
247+
result = cont(value)
248+
println("finalizing value=$value, got result=$result")
249+
result
250+
end)
251+
252+
# for convenience we also provide a `@ContextManager` macro which is the same as plain `ContextManager`,
253+
# however you can leave out the extra parantheses.
254+
context_ref(value) = @ContextManager function(cont)
255+
refvalue = Ref{Any}(value)
256+
println("setup Ref $refvalue")
257+
result = cont(refvalue)
258+
refvalue[] = nothing
259+
println("destroyed Ref $refvalue")
260+
result
261+
end
262+
263+
# we can try out a contextmanager, by providing `identity` as the continuation `cont`
264+
context_print("value")(identity) # 4
265+
# initializing value=4
266+
# finalizing value=4, got result=4
267+
268+
# or alternatively we can use Base.run
269+
run(context_ref(4)) # Base.RefValue{Any}(nothing)
270+
# setup Ref Base.RefValue{Any}(4)
271+
# destroyed Ref Base.RefValue{Any}(nothing)
272+
```
273+
274+
In a sense, these ContextManagers are a simple value with some sideeffects before and after. In functional programming
275+
it is key to cleanly captualize such side effects. That is also another reason why Option, Either, and Try are so
276+
common.
277+
278+
Using `Monadic.jl` we can actually work on ContextManagers as they would be values.
279+
```julia
280+
using Monadic
281+
flatmap(f, x) = Iterators.flatten(map(f, x))
282+
combined_context = @monadic map flatmap begin
283+
a = context_print(4)
284+
b = context_ref(a + 100)
285+
@pure a * b[]
286+
end
287+
288+
run(combined_context) # 416
289+
# initializing value=4
290+
# setup Ref Base.RefValue{Any}(104)
291+
# destroyed Ref Base.RefValue{Any}(nothing)
292+
# finalizing value=4, got result=416
293+
```
67294

68-
## [TODO] Other
295+
As you see, the contextmanager properly nest into one-another. And everything is well captured in the background by
296+
the `@monadic` syntax. This working pattern is very common and can also be used for `Option`, `Either` and `Try`.
297+
The syntax is called `monadic`, as `map` and `flatmap` define what in functional programming is called a `Monad` (think
298+
of it as a container which knows how to flatten out itself).
69299

70-
For abstraction purposes, there is also `Const` and `Identity` defined.
300+
The package `TypeClasses.jl` captures this idea in more depth. There you can also find a syntax `@syntax_flatmap`
301+
which refers to exactly the above use of `@monadic`.
71302

72-
Please see the tests `test/Const.jl` and `test/Identity.jl` respectively.
303+
For further details, don't hesitate to consult the source code `src/ContextManager.jl` or take a look at the tests
304+
`test/ContextManager.jl`.

src/DataTypesBasic.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ NOTE: in julia we can represent closed abstract types with two different systems
88
Approach 1.
99
===========
1010
11-
Advantages: Providing definitions for the concrete is actually enough for typeinference
11+
Advantages: Providing definitions for the concrete is actually enough for typeinference. More code reuse.
1212
Disadvantages: The typeinference like for `Dict(:a => Some(1), :b => None())` would infer
1313
`Dict{Symbol, Any}` instead of `Dict{Symbol, Union{Some, None}}`.
1414
1515
The internal details for this is that in many cases `Base.promote_typejoin(type1, type2)` is used to come up
1616
with a common type.
1717
While the type hierarchy is used for this, `Base.promote_typejoin` of two unrelated types will always result in `Any`.
1818
19+
You would need to overload `promote_typejoin` to support
20+
`Dict(:a => Identity(1), :b => nothing) isa Dict{Symbol, Option{Int}}`.
21+
1922
2023
Approach 2.
2124
===========
@@ -24,8 +27,9 @@ Advantages: `Dict(:a => Some(1), :b => None())` would indeed infer `Dict{Symbol,
2427
Disadvantages: you need to be careful to always implement functionalities first on separate functions unique to the
2528
sealed type, and then point generic functions to the specific one via `genericfunction(o::Option) = optionfunction(o)`
2629
27-
As Approach 2 is best with typeinference currently, and the overhead is at least restricted to kind-of-internal
28-
functions, we go with Approach 2. That applies to Option, Try and Either.
30+
31+
As we managed to make the promote_typejoin work as intended for `Union`, we follow Approach 1, in order to enable way
32+
higher code-reuse and datatype-reuse.
2933
"""
3034
module DataTypesBasic
3135

0 commit comments

Comments
 (0)