@hackage sliceofpy1.0.0

Python-ish slicing traversals for Haskell.

slice-of-py

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