# slice-of-py
![Slice Of Py](https://github.com/interosinc/sliceofpy/blob/master/misc/sliceofpy.png?raw=true "Slice of Py")
> "So the pie isn't perfect? Cut it into wedges. Stay in control, and never panic."
> - Martha Stewart
Bidirectional Python-ish slicing traversals for Haskell.
Many thanks to [Chris Penner](https://github.com/chrispenner) who did all of
the heavy lifting in creating the actual traversals.
## Introduction
This package provides traversals that allow addressing any `Traversable` using
python style slicing.
The cliff notes:
```haskell
"Slice of Py" ^.. sliced [s|2:5|] -- sliced + slice quasiquote
== "ice"
"Slice of Py" ^.. [sd|2:5|] -- sliced quasiquote
== "ice"
"Slice of Py" ^.. sliced "2:5" -- sliced + slice string
== "ice"
"Slice of Py" & partsOf [sd|2::2|] %~ reverse
== "Slyc ofePi"
"Slice of Pi" & [sd|:5|] %~ toUpper
== "SLICE of Pi"
[1..10] ^.. [sd|3::3|]
== [4,7,10]
[1..10] ^.. [sd|::-1|]
== [10,9,8,7,6,5,4,3,2,1]
```
Fundamentally Python slices are captured by the Haskell data type `(Maybe Int,
Maybe Int, Maybe Int)`. As such you can use this type directly as a slice but
writing out slices like `(Just 3, Nothing, Just (-1))` is fairly cumbersome so
we also provide the `Slice` class to treat other types as slices and provide
implementations for `(Int, Int Int)` as well as Strings (eg. `"1:10:2"`).
The `String` instance is convenient for use in ghci or small projects but lacks
type safety of course. If you provide a String that is not parseable into a
valid slice it won't be caught until runtime. Likewise a step size of zero,
which is an error, will not be caught until runtime.
To provide a type-safe middle ground between the more cumbersome tuple syntax
and the simpler string syntax we also provide an `s` quasiquoter (shown above)
that allows writing slices as `[s|1:2:3|]` as well as an `sd` quasiquoter that
fills in the `sliced` lens for you allowing eg `foo ^.. sliced ":5"` to be
written as `foo ^.. [sd|:5|]`.
With the quasiquoted versions anything that doesn't parse
as a valid slice (including step sizes of zero) will be caught at runtime.
The `sliced` traversal is an IndexedTraversal but it is created by conjoining
the actual indexed traversal and an non-indexed version so if the index ends up
being used it switches to the indexed version but otherwise has the performance
of the unindexed one.
In addition to the `sliced` function generates an appropriate traversal from
any instance of the `Slice` class, the `sliced'` function which generates an
appropriate traversal from three individual `Int` parameters (start, send and
step) is exposed.
## Differences from Python
### Slice Indices
NB. The tables in this section do not display correctly on Hackage. Please
[view the README on Github](https://github.com/interosinc/sliceofpy/blob/master/README.md)
to see them rendered properly.
Many slice operations will work identically to their python counterparts, eg:
| Python | Haskell |
|-----------------------------------------------------|-----------------------------------------------------|
| >>> "Slice of Py"[::]
"Slice of Py" | λ "Slice of Py" ^.. sliced "::"
"Slice of Py" |
| >>> "Slice of Py"[:3]
"Sli" | λ "Slice of Py" ^.. sliced ":3"
"Sli" |
| >>> "Slice of Py"[3:]
"ce of Py" | λ "Slice of Py" ^.. sliced "3:"
"ce of Py" |
| >>> "Slice of Py"[::2]
"Sieo y" | λ "Slice of Py" ^.. sliced "::2"
"Sieo y" |
| >>> "Slice of Py"[::-1]
"yP fo ecilS" | λ "Slice of Py" ^.. sliced "::-1"
"yP fo ecilS" |
| >>> "Slice of Py"[::-2]
"y oeiS" | λ "Slice of Py" ^.. sliced "::-2"
"y oeiS" |
| >>> "Slice of Py"[2:-2]
"ice of" | λ "Slice of Py" ^.. sliced "2:-2"
"ice of" |
| >>> "Slice of Py"[1:2]
"l" | λ "Slice of Py" ^.. sliced "1:2"
"l" |
| >>> "Slice of Py"[2:1]
"" | λ "Slice of Py" ^.. sliced "2:1"
"" |
| >>> "Slice of Py"[1:-1]
"lice of P" | λ "Slice of Py" ^.. sliced "1:-1"
"lice of P" |
| >>> "Slice of Py"[1:2:-1]
"" | λ "Slice of Py" ^.. sliced "1:2:-1"
"" |
| >>> "Slice of Py"[11::-2]
"y oeiS" | λ "Slice of Py" ^.. sliced "11::-2"
"y oeiS" |
| >>> "Slice of Py"[0:9]
"Slice of" | λ "Slice of Py" ^.. sliced "0:9"
"Slice of" |
| >>> "Slice of Py"[0:10]
"Slice of P" | λ "Slice of Py" ^.. sliced "0:10"
"Slice of P" |
| >>> "Slice of Py"[0:11]
"Slice of Py" | λ "Slice of Py" ^.. sliced "0:11"
"Slice of Py" |
| >>> "Slice of Py"[0:12]
"Slice of Py" | λ "Slice of Py" ^.. sliced "0:12"
"Slice of Py" |
But some things work differently:
| Python | Haskell |
|--------------------------------------------------------|--------------------------------------------------------|
| >>> "Slice of Py"[2:1:-1]
"i" | λ "Slice of Py" ^.. sliced "2:1:-1"
"l" |
| >>> "Slice of Py"[2::-1]
"ilS" | λ "Slice of Py" ^.. sliced "2::-1"
"lS" |
| >>> "Slice of Py"[2::-2]
"iS" | λ "Slice of Py" ^.. sliced "2::-2"
"l" |
| >>> "Slice of Py"[10::-2]
"y oeiS" | λ "Slice of Py" ^.. sliced "10::-2"
"Pf cl" |
| >>> "Slice of Py"[12:0:-1]
"yP fo ecil" | λ "Slice of Py" ^.. sliced "12:0:-1"
"yP fo ecilS" |
| >>> "Slice of Py"[11:0:-1]
"yP fo ecil" | λ "Slice of Py" ^.. sliced "11:0:-1"
"yP fo ecilS" |
| >>> "Slice of Py"[10:0:-1]
"yP fo ecil" | λ "Slice of Py" ^.. sliced "10:0:-1"
"P fo ecilS" |
| >>> "Slice of Py"[9:0:-1]
"P fo ecil" | λ "Slice of Py" ^.. sliced "9:0:-1"
"fo ecilS" |
As you can see, python slice notation gets awkward in certain edge cases as
described in [this stackoverflow
answer](https://stackoverflow.com/questions/509211/understanding-slice-notation)
whereas `sliced` uses a more consistent notation that lets you accomplish the
same thing as `::-1` while specifying all indices.
In addition since `sliced` is written in terms of `Traversable` you get slicing
for free on any `Traversable` type rather than having to implement
slice-specific interface like python's `__getitem__`.
Python supports assignment to some types, eg lists:
```python
>>> xs = [1,2,3,4,5]
>>> xs[:2] = [10,20]
>>> xs
[10, 20, 3, 4, 5]
```
But not all, eg strings:
```python
>>> s = "Slice of Pi"
>>> s[:5] = "Piece"
Traceback (most recent call last):
File "", line 1, in
TypeError: 'str' object does not support item assignment
```
Since `sliced` is a `Traversal` you can use the full power of the `lens`
library with it including replacing parts of lists:
```haskell
[1..5] & partsOf [sd|:2|] .~ [10,20]
== [10,20,3,4,5]
"Slice of Py" & partsOf [sd|9:3:-2|] .~ ['A'..]
== "SlicC BfAPy"
```
but also strings (or any other `Traversable`):
```haskell
"Slice of Py" & partsOf [sd|:5|] .~ "Piece"
== "Piece of Py"
"Slice of Py" & partsOf [sd|5::-1|] .~ repeat 'X'
== "XXXXX of Py"
let t = unfoldTree (\n -> (n, replicate n (n-1))) 3
putStr . drawTree . fmap show $ tree
3
|
+- 2
| |
| +- 1
| | |
| | `- 0
| |
| `- 1
| |
| `- 0
|
+- 2
| |
| +- 1
| | |
| | `- 0
| |
| `- 1
| |
| `- 0
|
`- 2
|
+- 1
| |
| `- 0
|
`- 1
|
`- 0
putStr . drawTree . fmap show $ (tree & [sd|2:5|] .~ 100)
3
|
+- 2
| |
| +- 100
| | |
| | `- 100
| |
| `- 100
| |
| `- 0
|
+- 2
| |
| +- 1
| | |
| | `- 0
| |
| `- 1
| |
| `- 0
|
`- 2
|
+- 1
| |
| `- 0
|
`- 1
|
`- 0
```
One significant deviation from the way Python's slices work is that in Python
if you assign a list of a different size to a slice then the original list will
be expanded or contracted to accomodate the size of the assigned slice:
```python
>>> xs = [1,2,3,4,5]
>>> xs[2:4] = [10,11,12,13,14,15]
>>> xs
[1, 2, 10, 11, 12, 13, 14, 15, 5]
>>> xs[2:8] = [100]
>>> xs
[1, 2, 100, 5]
```
Whereas with a Haskell traversal if you provide fewer elements than were
targeted then fewer elements will be overwritten and if you provide more
elements than were targeted the extra elements will be ignored:
```haskell
λ> [1,2,3,4,5] & partsOf (sliced "2:4") .~ [10..15]
[1,2,10,11,5]
λ> [1,2,10,11,12,13,14,15,5] & partsOf (sliced "2:8") .~ [100]
[1,2,100,11,12,13,14,15,5]
```
In addition to assignment/replacement you can of course use all of usual
suspects like `over` (`%~`) or the various lens helpers:
```haskell
λ> [1..10] & [sd|2:6|] %~ negate
[1,2,-3,-4,-5,-6,7,8,9,10]
λ> [1..10] & [sd|2:6|] *~ 10
[1,2,30,40,50,60,7,8,9,10]
λ> "Slice of Py" & [sd|:5|] %~ toUpper
"SLICE of Py"
λ> "Slice of Py" & partsOf [sd|::2|] %~ reverse
"yl co efiPS"
```
and of course you can chain on additional lens operations:
```haskell
[1..10] & [sd|2:6|] . filtered even *~ 10
== [1,2,3,40,5,60,7,8,9,10]
[1..10] ^.. droppingWhile (<5) [sd|3:7|]
== [5,6,7]
"Slice of Py" ^.. worded . [sd|:1|]
== "SoP"
productOf [sd|2:5|] [1..10]
== 60
```