Python-ish slicing traversals for Haskell.
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 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:
"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
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 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:
>>> xs = [1,2,3,4,5]
>>> xs[:2] = [10,20]
>>> xs
[10, 20, 3, 4, 5]
But not all, eg strings:
>>> s = "Slice of Pi"
>>> s[:5] = "Piece"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
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:
[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
):
"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:
>>> 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:
λ> [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:
λ> [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:
[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