Reputation: 883
I'm just wondering what is XL variable here, it's not declared anywhere? This function returns NONE if a string is not in the list. Otherwise, returns the original string list but without the string matched.
fun same_string(s1 : string, s2 : string) =
s1 = s2
fun all_except_option(s1: string, lst: string list) =
case lst of
[] => NONE
| x::xl => if same_string(s1, x) (* XL HERE ?? *)
then SOME xl
else case all_except_option(s1, xl) of
NONE => NONE
| SOME l => SOME (x::l)
Upvotes: 4
Views: 4232
Reputation: 183270
As melpomene commented above, the xl
on that line actually is the declaration of xl
.
In Standard ML, variables are declared (bound) by using patterns that are then matched to values. This is true even when using val
; for example, we can write:
val (x, y) = (1, "y")
to declare x
(binding it to 1
) and y
(binding it to "y"
).
In case
expressions and fn
expressions, the pattern match also doubles as a conditional; for example, this:
val rec sum =
fn nil => 0
| h :: t => h + sum t
creates a function that checks if its argument is nil
, in which case it does one thing, vs. if its argument has the form value1 :: value2
(meaning a non-empty list), in which case it does a different thing (using those values). (Hypothetically, if the argument were to have neither of those forms, then the function would end up raising Match
; but as it happens, any value of type int list
will perforce have one of those two forms, so the function will never raise Match
.)
Incidentally, note that that example uses the value identifiers nil
and ::
, but does not declare them; whereas it does declare the value identifiers h
and t
. This inconsistency may seem strange; the reason for it is that nil
and ::
are value constructors, meaning that they've been declared using datatype
(datatype 'a list = nil | :: of 'a * 'a list
), so they can be used in patterns. Something like this:
val nil = 3
is actually completely illegal, because nil
refers to the constructor that's already been declared, and the types of nil
and 3
are incompatible. By contrast, if an identifier is not already a value constructor, then appearance in a pattern constitutes a declaration (even if the identifier already is a value variable: the new declaration will simply hide the existing one). This can be a bit tricky for newcomers to the language, but with a little experience you'll find that it makes a lot of sense.
Upvotes: 5
Reputation: 16105
That seems like a useful library function. Here are a few pointers on how to improve it:
Don't write your own same_string
function that is exactly equivalent to the =
operator. Simply use the =
operator. Do this because of readability: Other SML programmers will identify the =
operator but will wonder if same_string
does something more. And why not let the function work for ''a and not just string?
(Minor thing) Don't name your functions after the type they return. The type itself should be sufficient annotation. I.e. you won't accidentally use this function and not realize it's actually returning an 'a list option.
After a while, the following pattern:
case f x of
NONE => NONE
| SOME y => SOME (g y)
becomes a little tedious and you want to abstract the SOME
/NONE
part away, since the same thing always happens (either it's a NONE
and nothing happens, or it's a SOME y
and something happens to the y
). Using the library function Option.map
with the type ('a → 'b) → 'a option → 'b option:
Option.map (fn y => g y) (f x)
It works just like when you map across a list. It might look like:
fun all_except (_, []) = NONE
| all_except (x1, x2::xs) =
if x1 = x2
then SOME xs
else Option.map (fn ys => x2::ys) (all_except (x1, xs))
And you might test it a little crudely like this:
val test_all_except_1 = all_except (4, [1,2,3]) = NONE
val test_all_except_2 = all_except (1, [1,2,3]) = SOME [2,3]
val test_all_except_3 = all_except (2, [1,2,3]) = SOME [1,3]
val test_all_except_4 = all_except (3, [1,2,3]) = SOME [1,2]
val test_all_except_5 = all_except (1, [1,1,1]) = SOME [1,1]
val test_all_except_6 = all_except (1, [1,2,1]) = SOME [2,1]
You may want to consider writing your functions in curried form:
fun curry f x y = f (x, y)
fun all_except _ [] = NONE
| all_except x1 (x2::xs) =
if x1 = x2
then SOME xs
else Option.map (curry op:: x2) (all_except x1 xs)
Here, all_except
takes its two arguments separate from one another, rather than in a (x1, x2::xs)
tuple/pair. And the value (fn ys => x2::ys)
has been rewritten using the helper function curry
in the following steps:
(fn ys => x2::ys) (* original *)
(fn ys => op:: (x2, ys)) (* converting :: to a function *)
(fn ys => curry op:: x2 ys) (* making it a curried function *)
(curry op:: x2) (* performing eta conversion *)
The last step might seem cryptic. The function curry op:: x2
is the one that takes a list and prepends x2
to it, so it has type 'a list → 'a list. If we temporarily called it f
, we'd have (fn ys => f ys)
, but that's just a silly way of writing f
.
Upvotes: 2