Does Haskell have List Slices (i.e. Python)?
ListHaskellSyntaxList Problem Overview
Does Haskell have similar syntactic sugar to Python List Slices?
For instance in Python:
x = ['a','b','c','d']
x[1:3]
gives the characters from index 1 to index 2 included (or to index 3 excluded):
['b','c']
I know Haskell has the (!!)
function for specific indices, but is there an equivalent "slicing" or list range function?
List Solutions
Solution 1 - List
There's no built-in function to slice a list, but you can easily write one yourself using drop
and take
:
slice :: Int -> Int -> [a] -> [a]
slice from to xs = take (to - from + 1) (drop from xs)
It should be pointed out that since Haskell lists are singly linked lists (while python lists are arrays), creating sublists like that will be O(to)
, not O(to - from)
like in python (assuming of course that the whole list actually gets evaluated - otherwise Haskell's laziness takes effect).
Solution 2 - List
If you are trying to match Python "lists" (which isn't a list, as others note) then you might want to use the Haskell vector package which does have a built in slice. Also, Vector
can be evaluated in parallel, which I think is really cool.
Solution 3 - List
No syntactic sugar. In cases where it's needed, you can just take
and drop
.
take 2 $ drop 1 $ "abcd" -- gives "bc"
Solution 4 - List
I don't think one is included, but you could write one fairly simply:
slice start end = take (end - start + 1) . drop start
Of course, with the precondition that start
and end
are in-bounds, and end >= start
.
Solution 5 - List
Python slices also support step:
>>> range(10)[::2]
[0, 2, 4, 6, 8]
>>> range(10)[2:8:2]
[2, 4, 6]
So inspired by Dan Burton's dropping every Nth element I implemented a slice with step. It works on infinite lists!
takeStep :: Int -> [a] -> [a]
takeStep _ [] = []
takeStep n (x:xs) = x : takeStep n (drop (n-1) xs)
slice :: Int -> Int -> Int -> [a] -> [a]
slice start stop step = takeStep step . take (stop - start) . drop start
However, Python also supports negative start and stop (it counts from end of list) and negative step (it reverses the list, stop becomes start and vice versa, and steps thru the list).
from pprint import pprint # enter all of this into Python interpreter
pprint([range(10)[ 2: 6], # [2, 3, 4, 5]
range(10)[ 6: 2:-1], # [6, 5, 4, 3]
range(10)[ 6: 2:-2], # [6, 4]
range(10)[-8: 6], # [2, 3, 4, 5]
range(10)[ 2:-4], # [2, 3, 4, 5]
range(10)[-8:-4], # [2, 3, 4, 5]
range(10)[ 6:-8:-1], # [6, 5, 4, 3]
range(10)[-4: 2:-1], # [6, 5, 4, 3]
range(10)[-4:-8:-1]]) # [6, 5, 4, 3]]
How do I implement that in Haskell? I need to reverse the list if the step is negative, start counting start and stop from the end of the list if these are negative, and keep in mind that the resulting list should contain elements with indexes start <= k < stop (with positive step) or start >= k > stop (with negative step).
takeStep :: Int -> [a] -> [a]
takeStep _ [] = []
takeStep n (x:xs)
| n >= 0 = x : takeStep n (drop (n-1) xs)
| otherwise = takeStep (-n) (reverse xs)
slice :: Int -> Int -> Int -> [a] -> [a]
slice a e d xs = z . y . x $ xs -- a:start, e:stop, d:step
where a' = if a >= 0 then a else (length xs + a)
e' = if e >= 0 then e else (length xs + e)
x = if d >= 0 then drop a' else drop e'
y = if d >= 0 then take (e'-a') else take (a'-e'+1)
z = takeStep d
test :: IO () -- slice works exactly in both languages
test = forM_ t (putStrLn . show)
where xs = [0..9]
t = [slice 2 6 1 xs, -- [2, 3, 4, 5]
slice 6 2 (-1) xs, -- [6, 5, 4, 3]
slice 6 2 (-2) xs, -- [6, 4]
slice (-8) 6 1 xs, -- [2, 3, 4, 5]
slice 2 (-4) 1 xs, -- [2, 3, 4, 5]
slice (-8)(-4) 1 xs, -- [2, 3, 4, 5]
slice 6 (-8)(-1) xs, -- [6, 5, 4, 3]
slice (-4) 2 (-1) xs, -- [6, 5, 4, 3]
slice (-4)(-8)(-1) xs] -- [6, 5, 4, 3]
The algorithm still works with infinite lists given positive arguments, but with negative step it returns an empty list (theoretically, it still could return a reversed sublist) and with negative start or stop it enters an infinite loop. So be careful with negative arguments.
Solution 6 - List
I had a similar problem and used a list comprehension:
-- Where lst is an arbitrary list and indc is a list of indices
[lst!!x|x<-[1..]] -- all of lst
[lst!!x|x<-[1,3..]] -- odd-indexed elements of lst
[lst!!x|x<-indc]
Perhaps not as tidy as python's slices, but it does the job. Note that indc can be in any order an need not be contiguous.
As noted, Haskell's use of LINKED lists makes this function O(n) where n is the maximum index accessed as opposed to python's slicing which depends on the number of values accessed.
Disclaimer: I am still new to Haskell and I welcome any corrections.
Solution 7 - List
Another way to do this is with the function splitAt
from Data.List
-- I find it makes it a little easier to read and understand than using take
and drop
-- but that's just personal preference:
import Data.List
slice :: Int -> Int -> [a] -> [a]
slice start stop xs = fst $ splitAt (stop - start) (snd $ splitAt start xs)
For example:
Prelude Data.List> slice 0 2 [1, 2, 3, 4, 5, 6]
[1,2]
Prelude Data.List> slice 0 0 [1, 2, 3, 4, 5, 6]
[]
Prelude Data.List> slice 5 2 [1, 2, 3, 4, 5, 6]
[]
Prelude Data.List> slice 1 4 [1, 2, 3, 4, 5, 6]
[2,3,4]
Prelude Data.List> slice 5 7 [1, 2, 3, 4, 5, 6]
[6]
Prelude Data.List> slice 6 10 [1, 2, 3, 4, 5, 6]
[]
This should be equivalent to
let slice' start stop xs = take (stop - start) $ drop start xs
which will certainly be more efficient, but which I find a little more confusing than thinking about the indices where the list is split into front and back halves.
Solution 8 - List
When I want to emulate a Python range (from m to n) in Haskell, I use a combination of drop & take:
In Python:
print("Hello, World"[2:9]) # prints: "llo, Wo"
In Haskell:
print (drop 2 $ take 9 "Hello, World!") -- prints: "llo, Wo"
-- This is the same:
print (drop 2 (take 9 "Hello, World!")) -- prints: "llo, Wo"
You can, of course, wrap this in a function to make it behave more like Python. For example, if you define the !!! operator to be:
(!!!) array (m, n) = drop m $ take n array
then you will be able to slice it up like:
"Hello, World!" !!! (2, 9) -- evaluates to "llo, Wo"
and use it in another function like this:
print $ "Hello, World!" !!! (2, 9) -- prints: "llo, Wo"
I hope this helps, Jon W.
Solution 9 - List
Why not use already existing Data.Vector.slice
together with Data.Vector.fromList
and Data.Vector.toList
(see https://stackoverflow.com/a/8530351/9443841)
import Data.Vector ( fromList, slice, toList )
import Data.Function ( (&) )
vSlice :: Int -> Int -> [a] -> [a]
vSlice start len xs =
xs
& fromList
& slice start len
& toList
Solution 10 - List
I've wrote this code that works for negative numbers as well, like Python's list slicing, except for reversing lists, which I find unrelated to list slicing:
slice :: Int -> Int -> [a] -> [a]
slice 0 x arr
| x < 0 = slice 0 ((length arr)+(x)) arr
| x == (length arr) = arr
| otherwise = slice 0 (x) (init arr)
slice x y arr
| x < 0 = slice ((length arr)+x) y arr
| y < 0 = slice x ((length arr)+y) arr
| otherwise = slice (x-1) (y-1) (tail arr)
main = do
print(slice (-3) (-1) [3, 4, 29, 4, 6]) -- [29,4]
print(slice (2) (-1) [35, 345, 23, 24, 69, 2, 34, 523]) -- [23,24,69,32,34]
print(slice 2 5 [34, 5, 5, 3, 43, 4, 23] ) -- [5,3,43]
Solution 11 - List
Obviously my foldl version loses against the take-drop approach, but maybe someone sees a way to improve it?
slice from to = reverse.snd.foldl build ((from, to + 1), []) where
build res@((_, 0), _) _ = res
build ((0, to), xs) x = ((0, to - 1), x:xs)
build ((from, to), xs) _ = ((from - 1, to - 1), xs)
Solution 12 - List
sublist start length = take length . snd . splitAt start
slice start end = snd .splitAt start . take end