Is there a way to ‘hide’ temporarily-unwanted, and potentially dangerous, values in a record?

This could be a long-shot but…

I have some code, below, which I have stripped down from the real code. (Apologies for the length but at least it can be evaluated in the FSI.)

I am reading some records from a database, converting them into Domain records, doing some modifications, and then saving them back to the database, but that’s probably not important.

When a MyThing value is of type SingleDigit, only the AllowZero value is valid.
When a MyThing value is of type MultiDigit, only the NumberOfDigits and Suffix values are valid.

In the code this works as expected but, I would like to be able to keep the AllowZero value and the Suffix value ‘behind the scenes’ when switching between the two in changeNumberOfDigits, so the previous values can be restored when the type changes rather than hard-coding some defaults, which isn’t currently possible with this design.

Basically, when the user reduces the number of digits to one I want to somehow keep the previous value of Suffix so it can be reinstated if the number of digits goes higher than one again. Same sort of thing for AllowZero but the other way round.

I did another version where I kept these fields in each type of MyThing, e.g.:

type SingleDigit = { AllowZero : bool ; Suffix : string option } 
type MultiDigit = { NumberOfDigits : int ; Suffix : string option ; AllowZero : bool }

…and just ignored them when they weren’t needed, but I was wondering if there was a better way.

I thought about storing the ‘missing’ values in other fields with different names (e.g. AllowZeroBackup in MultiDigit) but that’s not much better and could be even more confusing.

Every change will result in a database overwrite of the original record so using that as a ‘backup’ isn’t an option.

Holding the ‘missing’ values in another separate record would probably not be a good idea either, and it would certainly make things more complicated.

This code is only being, and will only be, used by me personally so as long as I document things nicely there probably won’t be a problem with the field duplication version but I would like to know if there is a better way in case I need it later.

And here’s the code:

open System 

type SingleDigit = { AllowZero : bool } 

type MultiDigit = { NumberOfDigits : int ; Suffix : string option }

type MyThing = 
    | Single of SingleDigit 
    | Multi of MultiDigit 

let changeNumberOfDigits number pattern = 
    match pattern, number with 
    | Single _, 1 -> pattern 
    | Single _, _ -> Multi { NumberOfDigits = number ; Suffix = Some "hello" } 
    | Multi _, 1 -> Single { AllowZero = false } 
    | Multi existing, _ -> Multi { existing with NumberOfDigits = number }

let toggleAllowZero pattern = 
    match pattern with 
    | Single { AllowZero = allowZero } -> Single { AllowZero = not <| allowZero } 
    | Multi m -> Multi m 

let changeSuffix newSuffix pattern = 
    match pattern with 
    | Single s -> Single s 
    | Multi m -> Multi { m with Suffix = newSuffix } 

let printValue = 
    let ofCharArray (chars : char array) = System.String(chars) 
    fun pattern -> 
        do 
            match pattern with 
            | Single {AllowZero = allowZero} when allowZero = true -> 
                printfn "0"
            | Single _ -> 
                printfn "5" 
            | Multi { NumberOfDigits = numberOfDigits ; Suffix = suffix } -> 
                let number = 
                    '9' 
                    |> Array.replicate numberOfDigits 
                    |> ofCharArray 
                suffix 
                |> Option.defaultValue String.Empty   
                |> (+) number  
                |> printfn "%s" 
        pattern 

Single { AllowZero = true } 
|> printValue 
|> changeNumberOfDigits 4 // AllowZero value is lost here.
|> printValue 
|> changeSuffix (Some "changed") 
|> printValue 
|> changeNumberOfDigits 1 // Suffix is lost here.
|> printValue 
|> toggleAllowZero 
|> printValue 
|> changeNumberOfDigits 6 // AllowZero value is lost here again.
|> printValue 
|> changeNumberOfDigits 1 // Suffix is lost here again.
|> printValue

You could play around with private record constructors. Something like

module Digits =
  // can only be constructed or have fields access within the same module
  type SingleDigit = private { AllowZero : bool; Suffix : string option } with 
    // new member to allow public access to the AllowZero field
    member this.AllowZero = this.AllowZero 

  type MultiDigit = private { NumberOfDigits : int; Suffix : string option; AllowZero : bool } with
    member this.NumberOfDigits = this.NumberOfDigits
    member this.Suffix = this.Suffix

  let initializeNewSingleDigit allowZero = 
    { AllowZero = allowZero; Suffix = None }

  let initializeNewMultiDigit numDigits suffix = 
    { NumberOfDigits = numDigits; Suffix = suffix; AllowZero = false }

  type MyThing = 
    | Single of SingleDigit 
    | Multi of MultiDigit   
  
  let changeNumberOfDigits number pattern = 
    match pattern, number with 
    | Single _, 1 -> pattern 
    | Single s, _ -> Multi { NumberOfDigits = number ; Suffix = s.Suffix } 
    | Multi m, 1 -> Single { AllowZero = m.AllowZero } 
    | Multi existing, _ -> Multi { existing with NumberOfDigits = number }

  ...

WARNING: I didn’t run that through a compiler, so it might not work. Just a starting point.

From outside the Digits module, you don’t have access to do .Suffix on a SingleDigit, or to do .AllowZero on a MultiDigit, AND you also don’t have access to construct values or to do copy & update. If you need more fine-grained control than that, you’re probably going to need to abandon records and use a class.

1 Like

That’s an interesting possible solution.

I’m having a bit of trouble getting a version that will compile but I’ll carry on and see if I can get somewhere with it.

Cheers.

Yeah, made a couple mistakes in there. I think the big thing is you can’t have a member with the same name as one of the record fields. I just made all the (private) record fields lowercase which breaks convention, but works.
Here’s a complete version:

open System

module Digits =
  // can only be constructed or have fields access within the same module
  type SingleDigit = private { allowZero : bool; suffix : string option } with 
    // new member to allow public access to the AllowZero field
    member this.AllowZero = this.allowZero 

  type MultiDigit = private { numberOfDigits : int; suffix : string option; allowZero : bool } with
    member this.NumberOfDigits = this.numberOfDigits
    member this.Suffix = this.suffix

  let initializeNewSingleDigit allowZero = 
    { allowZero = allowZero; suffix = None }

  let initializeNewMultiDigit numDigits suffix = 
    { numberOfDigits = numDigits; suffix = suffix; allowZero = false }

  type MyThing = 
    | Single of SingleDigit 
    | Multi of MultiDigit   
  
  let changeNumberOfDigits number pattern = 
    match pattern, number with 
    | Single _, 1 -> pattern 
    | Single s, _ -> Multi { numberOfDigits = number ; suffix = s.suffix; allowZero = s.AllowZero } 
    | Multi m, 1 -> Single { allowZero = m.allowZero; suffix = m.Suffix } 
    | Multi existing, _ -> Multi { existing with numberOfDigits = number }

  let toggleAllowZero pattern = 
    match pattern with 
    | Single ({ allowZero = allowZero } as s) -> Single { s with allowZero = not <| allowZero } 
    | Multi m -> Multi m 

  let changeSuffix newSuffix pattern = 
    match pattern with 
    | Single s -> Single s 
    | Multi m -> Multi { m with suffix = newSuffix } 

open Digits
// from here down, you can't access the `suffix` field of a `SingleDigit`, 
// or the `allowZero` field of a `MultiDigit`

let printValue = 
  let ofCharArray (chars : char array) = System.String(chars) 
  fun pattern -> 
    do 
      match pattern with 
      | Single s when s.AllowZero = true -> 
        printfn "0"
      | Single _ -> 
        printfn "5" 
      | Multi m -> 
        let number = 
          '9' 
          |> Array.replicate m.NumberOfDigits 
          |> ofCharArray 
        m.Suffix 
        |> Option.defaultValue String.Empty   
        |> (+) number  
        |> printfn "%s" 
    pattern 


Single (initializeNewSingleDigit true)
|> printValue 
|> changeNumberOfDigits 4
|> printValue 
|> changeSuffix (Some "changed") 
|> printValue 
|> changeNumberOfDigits 1
|> printValue 
|> toggleAllowZero 
|> printValue 
|> changeNumberOfDigits 6
|> printValue 
|> changeNumberOfDigits 1
|> printValue
|> ignore

Apologies for the delay in my reply.

Thanks for sticking with this; your new version looks interesting.

There are a couple of things that I’m not sure about, e.g.
type TypeName = private {...} (never seen the private in that position before)
…and…
| Single ({ allowZero = allowZero } as s) (never seen that combination before)
…so I’ll need to look at the documentation to see how they work.

Once I’m a bit clearer on what’s happening I try the same technique the real code and see what happens.

The type TypeName = private { ... } is the main F# records feature I’m trying to showcase with the example I posted. That means that you can only access most of the built-in record functionality from within the same module or namespace that the record was defined in. External to the module or namespace, you can’t construct it with normal record syntax, you can’t access any of the fields (this is the desired trait for your use), and you can’t copy-and-update ({ x with ... }). You can though write members for all the public functionality you want to expose, or functions within the same module/namespace. If you want more fine-grained control over what’s public and private than that, you’ll have to abandon records and move towards classes.

As for | Single ({ allowZero = allowZero } as s):
In your original code, you had the line

| Single { AllowZero = allowZero } -> Single { AllowZero = not <| allowZero } 

So I’ll assume you know how destructuring a record works. I had to change the field name from AllowZero to allowZero because I wanted a public member AllowZero. The as s there means that while I want to destructure the record to extract the allowZero value, I also want to be able to refer to the whole record as s. I could have just skipped destructuring and done

| Single s -> Single { s with allowZero = not <| s.allowZero }

Again, apologies for another delay in my reply, and thanks for sticking with this.

The concern I had with private was where it was in that line of code.

The official documentation I was looking at Records in F# | Microsoft Learn shows the accessibility modifier before the type name, not after the equals sign, and so I was confused as to whether your example was a special case which I couldn’t find in the documentation.

As for the other code, I’d just never seen de-structuring and binding (if that’s the correct term here) done at the same time.

It just ‘threw me’ a bit is all but it makes sense once I understand what’s going on.

Anyway, the code you gave looks like it works just fine so I’ll be taking my cue from that when I get back to working on the real code.

Cheers.

Yeah type private MyRecord { A : int } and type MyRecord private { A : int } are different things, and I’m having trouble finding any documentation on the latter - I can only find StackOverflow posts talking about it, such as this one. The former makes the whole type private, so for example, you couldn’t even reference MyRecord outside of the same module/namespace. The latter only makes the constructor and the fields private, so you can still reference MyRecord from elsewhere, but you can’t make a new one and really do anything with an existing one unless it has some public members declared on it.

Ah, now I get it.

After reading your explanations again I finally see what you’ve been trying to tell me all along.

I vaguely remembered something similar and then looked back into Scott Wlaschin’s book on Domain Modelling and found it there, in the chapter on Domain Integrity regarding the definition and validation of Simple Value types.

I’d read the book a few times but, because there are so many new (to me) things to learn in there, I just forgot about that particular ‘wrinkle’.

Now I understand (up to a point) what’s happening here, and I can also see where I can use it in other places.

Thanks again; your help is much appreciated.

1 Like