1
\$\begingroup\$

In the context of a library to generate random credit card, it would be nice to let the user the possibility to set some optional options on some cards. Lets take Visa. Two lengths are available: 13 or 16 digits.

In C# a common implementation would be:

enum VisaLengthOptions
{
    Thirteen,
    Sixteen
}

public static string GenerateVisa(VisaLengthOptions length = VisaLengthOptions.Sixteen) {
    if (length == VisaLengthOptions.Thirteen) return "4000000000001";
    if (length == VisaLengthOptions.Sixteen) return "4000000000000001";
    return "";
}

Since F# does not support optional parameter (for currying and simplicity reason (note that it could be done since ocaml did it)). What would be the most idiomatic way to write this in F#?

Here is a guess:

type VisaLengthOptions =
    | Thirteen = 0
    | Sixteen = 1

let constructVisa length =
    match length with
    | VisaLengthOptions.Thirteen -> "4000000000001"
    | VisaLengthOptions.Sixteen -> "4000000000000001"
    | _ -> ""

let generateVisa () = constructVisa VisaLengthOptions.Sixteen

So there are no optional parameter and have two functions cant have the same name even with different signatures. What wrong with this snippet?

We want our library to have a Truly Discoverable API. So we would like to avoid having two different name for basically the same logic. Should we follow the map and map2 and goes generateVisa and generateVisa2? or like map and mapi and goes generateVisa and generateVisaL? Is there a one true F# way?

A good example in the .net world is Split.

\$\endgroup\$
2
  • \$\begingroup\$ Just for the sake of being pedantic: credit card numbers are not "random": usually the first six digits represent the BIN, that is a prefix that is assigned to issuers (of course large issuers have more than one BIN). Besides, the card number should have a check-digit to comply with the Luhn algorithm. So, the issuer has an effective pool that is not 10^16. Then, it stands to reason that the same number should not be assigned more than once - unlikely with such a large range but depends on the seed. If you decide to have randomization you should still have some form of storage. \$\endgroup\$
    – Kate
    Mar 20, 2021 at 22:31
  • \$\begingroup\$ @Anonymous Well yes but no. If most card comply with the Luhn algorithm some don't (e.g. Verve or Diners Club enRoute credit card dont). For the 6 first numbers this is far from being true. Any card starting with a 4 may be a Visa (and a visa doesn't need anything else than starting with a four to be a visa) but if it is 4571 it is specifically a Dankort (Visa co-branded). Dankort also uses 5019. So the prefix can vary from 1 to 6. Finally you may or may not care about collision. it depends of your use-case. \$\endgroup\$
    – aloisdg
    Mar 21, 2021 at 14:39

2 Answers 2

2
\$\begingroup\$

I would argue that if you need optional parameters your interface is different hence you need another function. With that said, if you really want optional parameters you must always define a default value, an you could then use partial application for that.

let f op1 op2 nonOpt = op1 + op2 + nonOpt

let fWithDefaults = f 1 2

Then you can use your fWithDefaults whenever you want the defaults.

For your particular example I would write it like this :

type VisaLengths = 
     | Thirteen
     | Sixteen

let generateVisa (length : VisaLengths option) =
     let length = Option.defaultValue Sixteen length
     match length with
     | Thirteen -> "4000000000001"
     | Sixteen -> "4000000000000001"
      

It would be the most "idiomatic", even if it can be a litter cumbersome but it does what you want to do.

Agree, you still have to passe None... But at least your intention about a optional value is explicit, which is very valuable for others reading your code.

\$\endgroup\$
1
\$\begingroup\$

First, I don't think you want an enum there, but I don't have the full context of your actual usage, my examples use a Discriminated Union rather than an enum.

I'm not sure which is idiomatic but here are 2.5 ways to do it.

type VisaLengthOptions = 
    | Thirteen
    | Sixteen

let generateVisa =
    function
    | Some Thirteen -> "4000000000001"
    | Some Sixteen
    | None -> "4000000000000001"
    
    
type Generator() =
    member __.GenerateVisa(?visaLength) =
        let visaLength = defaultArg visaLength Sixteen
        match visaLength with
        | Thirteen -> "4000000000001"
        | Sixteen -> "4000000000000001"

There is also the attribute based option presented in

https://github.com/fsharp/fslang-design/blob/master/FSharp-4.1/FS-1027-complete-optional-defaultparametervalue.md

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.