Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Can I use negative indices on Arrays. (like in Python)?

Tags:

f#

Edit 2021:

Yes you can since F#5.0 see Docs

Original Question:

I would like to use negative indices on Arrays so that i can write myThings.[-2] <- "sth" to set the second last item. Is this possible?

I tried this but it fails to compile with:

Method overrides and interface implementations are not permitted here

type ``[]``<'T>  with  
    /// Allows for negative index too (like python)
    override this.Item
        with get i =   if i<0 then this.[this.Length+i]      else this.[i]
        and  set i v = if i<0 then this.[this.Length+i] <- v else this.[i] <- v

I know, I could use new members like myThings.SetItemNegative(-2,"sth") but this is not as nice as using the index notation:

type ``[]``<'T>  with  
    /// Allows for negative index too (like python)
    member this.GetItemNegative (i) =
          if i<0 then this.[this.Length+i]      else this.[i]
    
    member this.SetItemNegative (i,v) =
          if i<0 then this.[this.Length+i] <- v else this.[i] <- v
like image 973
Goswin Rothenthal Avatar asked Jan 26 '23 19:01

Goswin Rothenthal


2 Answers

Unfortunately existing methods in a type have priority over future extension members.

It doesn't make so much sense but that's the way currently is, you can read more about it in this issue: https://github.com/dotnet/fsharp/issues/3692#issuecomment-334297164

That's why if you define such extension it will be ignored, and what's worst: silently ignored !

Anyway there are some proposals to add something similar to negative slices to F#.

like image 196
Gus Avatar answered Feb 03 '23 04:02

Gus


Gus explained that existing members of 'T array can not be overwritten. A workaround is extending 'T seq. For my F# scripts this is good enough. I am not sure if this is a good idea in general though.

open System
open System.Collections.Generic
open System.Runtime.CompilerServices
//[<assembly:Extension>] do()

/// Converts negative indices to positive ones 
/// e.g.: -1 is  last item .
let negIdx i len =
    let ii =  if i<0 then len+i else i
    if ii<0 || ii >= len then failwithf "Cannot get index %d of Seq with %d items" i len
    ii

let get i (xs:seq<'T>) : 'T =        
    match xs with
    //| :? ('T[])  as xs -> xs.[negIdx i xs.Length] // covered by IList
    | :? ('T IList)  as xs -> xs.[negIdx i xs.Count]
    | :? ('T list)   as xs -> List.item (negIdx i (List.length xs)) xs
    | _ -> Seq.item  (negIdx i (Seq.length xs)) xs

let set i x (xs:seq<_>) :unit =
    match xs with
    | :? ('T[])       as xs -> xs.[negIdx i xs.Length]<- x // why not covered by IList?
    | :? ('T IList)   as xs -> xs.[negIdx i xs.Count] <- x
    | _ -> failwithf "Cannot set items on this Seq (is it a dict, lazy or immutable ?)"


//[<Extension>]
type Collections.Generic.IEnumerable<'T>  with 

    [<Extension>] 
    ///Allows for negtive indices too (like Python)
    member this.Item 
        with get i   = get i this
        and  set i x = set i x this

    ///Allows for negative indices too.
    ///The resulting seq includes the item at slice-ending-index. like F# range expressions include the last integer e.g.: 0..5
    [<Extension>]
    member this.GetSlice(startIdx,endIdx) : 'T seq = // to use slicing notation e.g. : xs.[ 1 .. -2]
        let count = Seq.length this
        let st  = match startIdx with None -> 0        | Some i -> if i<0 then count+i      else i
        let len = match endIdx   with None -> count-st | Some i -> if i<0 then count+i-st+1 else i-st+1

        if st < 0 || st > count-1 then 
            let err = sprintf "GetSlice: Start index %d is out of range. Allowed values are -%d up to %d for Seq of %d items" startIdx.Value count (count-1) count
            raise (IndexOutOfRangeException(err))

        if st+len > count then 
            let err = sprintf "GetSlice: End index %d is out of range. Allowed values are -%d up to %d for Seq of %d items" endIdx.Value count (count-1) count
            raise (IndexOutOfRangeException(err)) 

        if len < 0 then
            let en =  match endIdx  with None -> count-1 | Some i -> if i<0 then count+i else i
            let err = sprintf "GetSlice: Start index '%A' (= %d) is bigger than end index '%A'(= %d) for Seq of %d items" startIdx st endIdx en  count
            raise (IndexOutOfRangeException(err)) 

        this |> Seq.skip st |> Seq.take len

// usage :

let modify (xs:seq<_>) =
    xs.[-1] <- 0 // set last
    xs.[-2] <- 0 // set second last
    xs

let slice (xs:seq<_>) =
    xs.[-3 .. -1] // last 3 items


modify [|0..9|] 
slice  [|0..9|] 
like image 45
Goswin Rothenthal Avatar answered Feb 03 '23 03:02

Goswin Rothenthal