slice-of-py
"So the pie isn't perfect? Cut it into wedges. Stay in control, and never panic."
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