0

As an exercise in class, we are supposed to calculate the entry price for people in a night club, provided their age and gender. The under 25 get 20% off, and Females/NBs get 50% off, stacking multiplicatively.

While my code works, it repeats the gender check twice, which is poor form and could cause problems in a more complex application. How can I spare the repetition ?

(* OCaml Version *)
let entry_price age gender =
    if age < 18 
    then (failwith "Must be over 18 to enter")
    else let price = 12.0 in
      if age <= 25
      then let price = (price *. 0.8) in
        if gender == 'f' || gender == 'x'
        then (price *. 0.5)
        else prix
      else if gender == 'f' || gender == 'x'
        then (price *. 0.5)
        else price;;

Here is a Python version that does not repeat itself, thanks to ternary operators : (Could also be done with non-nested ifs, which Ocaml disallows)

# Python version
def entry_price(age : int, gender : str):
    if age < 18:
        raise ArgumentError("Must be over 18 to enter")
    return ( 
        12.0
        *(0.8 if (age<25) else 1)
        *(0.5 if gender in ['f', 'x'] else 1)
    )

ice-wind
  • 690
  • 4
  • 20

2 Answers2

5

You can pretty much copy the python version:

let entry_price age gender =
  if age < 18 
  then failwith "Must be over 18 to enter"
  else
    12.0
    *. (if age < 25 then 0.8 else 1.0) 
    *. (if gender = 'f' || gender = 'x' then 0.5 else 1.0);;

There's only a slight difference in the if expression syntax, and you need to use the float-specific multiplication operator, *., instead of the int-specific *.

Also, to pick a nit, there are no statements in OCaml. Statements are an imperative/procedural construct, ie. do this, then do that. OCaml is an expression-based language, where each expression evaluates to a value. You can still discard the value of an expression, and thereby make it look like a statement, but the compiler will complain unless you are very explicit about discarding it since that is usually a user error.

glennsl
  • 28,186
  • 12
  • 57
  • 75
  • I note you used `=` rather than `==` as in the OP's code. Might be worth mentioning and explaining why. – Chris Feb 18 '22 at 15:26
  • @Chris is == even valid in ocaml ? We have only seen the single = in class, and I think I remember using == being a syntax error – ice-wind Feb 18 '22 at 15:47
  • 1
    https://stackoverflow.com/questions/13590307/whats-the-difference-between-equal-and-identical-in-ocaml – Chris Feb 18 '22 at 15:48
  • Alright, I guess that works. It still seems very confusing to me that everything has to be a single **expression** which does everything at once, for now it still seems like a contrivance / party trick (everything is technically a one-liner). I guess I’m just very used to OO / Imperative thinking. – ice-wind Feb 18 '22 at 15:52
  • @Chris seems like == in ocaml is like Swift’s === then – ice-wind Feb 18 '22 at 15:53
  • It's a major change in mindset. If you keep practicing, it will click eventually for you. – Chris Feb 18 '22 at 17:07
4

@glennsl provides a nice direct translation of the Python code to OCaml, but there are other ways we can approach this.

If we create a variant type to describe ages, we can write a categorize_age function which will translate a numeric age to a descriptive term we can pattern match on. It also gives us a convenient place in the future to change what ages we use to make these determinations.

Then we can use a match on both age and gender to consider the four possible outcomes:

  • Under 18
  • Under 25 and either 'f' or 'x' gender
  • 25 or older and either 'f' or 'x' gender
  • Anyone else 18 or older
type age_cat = Under_age | Legal | Adult

let categorize_age a = 
  if a < 18 then Under_age
  else if a <= 25 then Legal
  else Adult
        
let entry_price age gender = 
  let price = 12.0 in
  match categorize_age age, gender with  
  | Under_age, _       -> failwith "Must be over 18 to enter"
  | Legal, ('f' | 'x') -> price *. 0.8 *. 0.5 (* or just 0.4  *)
  | Adult, ('f' | 'x') -> price *. 0.5 *. 0.5 (* or just 0.25 *)
  | _                  -> price

Looking at the logic here, we actually don't need to specify Adult, ('f' | 'x') because we know that we've already matched cases where when gender is 'f' or 'x', age is either Under_age or Legal. Thus we can use a wildcard _ for the pattern: _, ('f' | 'x').

The last pattern is a wildcard because we're matching any case that hasn't already been matched, and those values are not relevant to the outcome, so there's no point in binding name(s) to them.

let entry_price age gender = 
  let price = 12.0 in
  match categorize_age age, gender with  
  | Under_age, _       -> failwith "Must be over 18 to enter"
  | Legal, ('f' | 'x') -> price *. 0.8 *. 0.5 (* or just 0.4  *)
  | _,     ('f' | 'x') -> price *. 0.5 *. 0.5 (* or just 0.25 *)
  | _                  -> price
Chris
  • 26,361
  • 5
  • 21
  • 42
  • While this is probably worse than @glennsl ‘s solution for this particular example, it does seem more versatile, especially for non-arithmetic applications. – ice-wind Feb 18 '22 at 16:01
  • 1
    I think it more directly shows the relationship between the `age` and `gender` inputs to the `entry_price` function, and what they map to. – Chris Feb 18 '22 at 16:06