13

I have a module that works on paths represented as lists. Most of the functions do typical recursive list processing, but now I need one that sometimes mutates a path. So, I wrote this replace function:

module List =
  let replace f sub xs = 
    let rec finish acc = function
      | [] -> acc
      | x::xs -> finish (x::acc) xs
    let rec search acc = function
      | [] -> None
      | x::xs -> 
        if f x then Some(finish ((sub x)::xs) acc)
        else search (x::acc) xs
    search [] xs

which works like this:

let xs = List.init 10 id
let res = List.replace ((=) 5) (fun _ -> -1) xs
//Some [0; 1; 2; 3; 4; -1; 6; 7; 8; 9]

Usually, when I feel the need to augment a built-in module I ultimately discover I'm doing something quirky or inefficient. Is replacing a list element one of those things? Is there a simpler (equally efficient) way to do this?

Daniel
  • 47,404
  • 11
  • 101
  • 179
  • 5
    You are not mutating the list, but constructing the result of the replacement. Is `O(N)` complexity acceptable for your application? If yes, your code is perfect, if no, you have two options: use a mutable DS such as an array with `O(1)` update but `O(N)` clone; or else search for purely-functional random-access lists in the literature (e.g. Okasaki). – t0yv0 Jul 12 '12 at 16:09
  • 1
    @toyvo: I thought it was obvious I'm not mutating the list (in the technical sense) since that isn't possible. Reconstruction is the only option. The question is, does this operation indicate the wrong DS is being used? The search itself is _O(N)_ (and I don't need to improve on that) so using an array wouldn't change the asymptotic complexity (although it would negligibly improve the actual cost). – Daniel Jul 12 '12 at 19:54
  • @Daniel sorry about picking on that word. Yes, I have not payed attention closely, if you are doing linear search then neither arrays nor RA lists will help any. Then lists are the right (simple) DS. Could you order your elements? This would put `log(N)` search on the table, and, say, maps for log updates. – t0yv0 Jul 12 '12 at 20:29
  • 1
    Maybe I'm being overly apprehensive. The lack of a built-in function made me think I might have missed something obvious. – Daniel Jul 12 '12 at 20:44
  • @Daniel, curiously both SML and OCaml standard libs don't have it: http://www.standardml.org/Basis/list.html and http://caml.inria.fr/pub/docs/manual-ocaml/libref/List.html - I think it's just because it's `O(N)`. OCaml has assoc-list support - that does linear search too. But it does not have replace, I guess with assoc lists you can accomplish the same effect with `(::)`. – t0yv0 Jul 12 '12 at 20:46
  • @toyvo: Your initial comment comes the closest to addressing the question. Since no other opinions were given, if you make yours an answer I'll accept. – Daniel Jul 14 '12 at 21:13
  • thanks, wrote an answer with a summary of the above discussion. – t0yv0 Jul 14 '12 at 21:30

3 Answers3

7

If O(N) complexity is acceptable for your application, your code is perfect. For better complexity you would want to work around the need to do linear search, for example by imposing order on the elements and using binary search trees.

A related problem not involving search is replacing a list element with a known index:

val replaceAt : int -> 'a -> 'a list -> 'a list

For this problem, better persistent data structures exist than the standard list. Search for purely-functional random-access lists in the literature.

Curiously, no ML-family language (OCaml, F#, SML) defines replace or replaceAt in the standard list library. This is probably meant to encourage users to redesign their code to avoid the O(N) complexity of these operations.

t0yv0
  • 4,714
  • 19
  • 36
5

You can write it using List.fold:

let replace f sub xs = 
  let processItem (found,list) x =
    if found then (true,x::list) 
    elif f x then (true,(sub x)::list) 
    else (false,x::list)
  let (found, list) = xs |> List.fold processItem (false,[])
  if found then Some(List.rev list)
  else None

It is slightly simpler and with similar performance (one single loop over the elements of the list).

MiMo
  • 11,793
  • 1
  • 33
  • 48
2
let replace pf el xs =
  let c = ref 0
  let xs = List.map (fun x -> if pf x then incr c;el else x) xs
  if !c = 0 then None else Some xs

(*
> replace ((=)5) -1 [0..9];;
val it : int list option = Some [0; 1; 2; 3; 4; -1; 6; 7; 8; 9]
> replace ((=)10) -1 [0..9];;
val it : int list option = None
*)

UPDATE

let replace pf sf xs =
  let find = ref false
  let rec aux = function
    | [] -> []
    | x::xs -> if pf x then find := true;(sf x) :: xs else x :: (aux xs)
  let xs = aux xs
  if !find then Some xs else None
(*
> let xs = [0..9];;
val xs : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9]
> let subf = fun _ -> -1;;
val subf : 'a -> int

> replace ((=) 5) subf xs;;
val it : int list option = Some [0; 1; 2; 3; 4; -1; 6; 7; 8; 9]
> replace ((<) 5) subf xs;;
val it : int list option = Some [0; 1; 2; 3; 4; 5; -1; 7; 8; 9]
> replace ((=) 50) subf xs;;
val it : int list option = None
*)
BLUEPIXY
  • 39,699
  • 7
  • 33
  • 70
  • Yes, what I'm doing is essentially a map, but I shied away from `List.map` because it requires mutability to track if the list changed. Much shorter though. +1 – Daniel Jul 13 '12 at 03:58
  • 1
    This code does something different: it replaces ALL elements matching the predicate `pf`, whereas the code in the question replaces only the first occurence (easily fixed doing `if !c=0 && (pf x) . . .` though) – MiMo Jul 13 '12 at 07:33
  • @Daniel - Well, I think that it substantially the same. – BLUEPIXY Jul 13 '12 at 07:48
  • @MiMo - Thank you pointed out. I was misunderstanding the behavior of the replace of Daniel. However, the change seems easy, as already shown. – BLUEPIXY Jul 13 '12 at 07:51
  • @BLUEPIXY: For my purposes the difference doesn't matter since there will always be 0 or 1 matching element. – Daniel Jul 13 '12 at 14:05
  • @Daniel - I do not know what will such a comment in what. – BLUEPIXY Jul 13 '12 at 14:44