a_a
a_a

Reputation: 271

How to make F# modules configurable (or make similar effect, like object properties)

Let's say I have these code:

namespace global
module NumeracyProblemsInTen=
    module private Random=
        let private a=lazy System.Random()
        let getRandom()=a.Force()
        let next()=getRandom().Next()
        let lessThan exclusiveMax=getRandom().Next exclusiveMax
        let pick seq=
            assert(not<|Seq.isEmpty seq)
            lessThan<|Seq.length seq|>Seq.item<|seq
    module UsedNumbers=
        let min,max=1,9 // *want to make these data variable*
        let numbers={min..max}
        let atLeast a=numbers|>Seq.skipWhile((>)a)
        let atMost a=numbers|>Seq.takeWhile((>=)a)
        module Random=
            let private pick=Random.pick
            let pickNumber()=pick numbers
            let atMost=atMost>>pick
    open UsedNumbers
    module AdditionInTen=
        module Addends=
            let max=max-min
            let numbers={min..max}
            let pick()=Random.pick numbers
        open Addends
        let quiz()=
            let addend=pick()
            addend,Random.atMost<|min+max-addend
        let calc(addend,another)=addend+another
    module MultiplyInTen=
        let quiz()=
            let multipiler=Random.pickNumber()
            multipiler,Random.pick{min..max/multipiler}
        let calc(multipiler,another)=multipiler*another
    module SubtractionInTen=
        let minSubtrahend,minResult=min,min
        let minMinuend=minSubtrahend+minResult
        let minuends=atLeast minMinuend
        let quiz()=
            let minuend=Random.pick minuends
            minuend,Random.pick{minSubtrahend..minuend-minResult}
        let calc(minuend,subtrahend)=minuend-subtrahend
    module DeviditionInTen=
        let devisible devidend deviser=devidend%deviser=0
        let findDevisers devidend=numbers|>Seq.filter(devisible devidend)
        let findDeviditions devidend=findDevisers devidend|>Seq.map(fun deviser->devidend,deviser)
        let problems=Seq.collect findDeviditions numbers
        let quiz()=Random.pick problems
        let calc(devidend,deviser)=devidend/deviser
    type Problem=Addition of int*int|Subtraction of int*int|Multiply of int*int|Devidition of int*int
    let quiz()=
        let quizers=[AdditionInTen.quiz>>Addition;SubtractionInTen.quiz>>Subtraction;
            MultiplyInTen.quiz>>Multiply;DeviditionInTen.quiz>>Devidition]
        quizers|>Random.pick<|()
    let calc problem=
        match problem with
            |Addition(addend,another)->AdditionInTen.calc(addend,another)
            |Subtraction(minuend,subtrahend)->SubtractionInTen.calc(minuend,subtrahend)
            |Multiply(multipiler,another)->MultiplyInTen.calc(multipiler,another)
            |Devidition(devidend,deviser)->DeviditionInTen.calc(devidend,deviser)
module NumeracyProblemsUnderOneHundred=
    module UsedNumbers=
        let min,max=1,99
        // ...
    // ...
    // ...
    // OMG! Do I must copy all the previous code here?

If I use oo/types, I can simply define Max as a property, is there a good way to resolve the same scene without object/types but only modules/immutable bindings way? A bit of more complex scene should be also considered, more configurable data, with more usage in different ways.

Upvotes: 2

Views: 118

Answers (1)

TheInnerLight
TheInnerLight

Reputation: 12184

So, it seems to me that your code is designed to generate a random mathematical operation which you can then calculate the result of. I found this code quite difficult to decipher, it appears that you're trying to use modules like object oriented classes which contain internal state and that isn't really a good way to think about them.

You can achieve much more granular code reuse by thinking about smaller, composable, units of code.

Here is my attempt at this problem:

type Range = {Min : int; Max : int}

type Problem= 
    |Addition of int*int
    |Subtraction of int*int
    |Multiplication of int*int
    |Division of int*int

module NumeracyProblems =
    let private rnd = System.Random()

    let randomInRange range = rnd.Next(range.Min, range.Max+1)

    let isInRange range x = x >= range.Min && x <= range.Max

    let randomOpGen() = 
        match randomInRange {Min = 0; Max = 3} with
        |0 -> Addition
        |1 -> Subtraction 
        |2 -> Multiplication
        |3 -> Division

    let calc = function
        |Addition (v1, v2) -> Some(v1 + v2)
        |Subtraction (v1, v2) -> Some(v1 - v2)
        |Multiplication (v1, v2) -> Some(v1 * v2)
        |Division (v1, v2) -> 
            match v1 % v2 = 0 with 
            |true -> Some(v1 / v2)
            |false -> None

    let quiz range =
        let op = randomOpCtor()
        let optionInRange x =
            match isInRange range x with
            |true -> Some x
            |false -> None
        Seq.initInfinite (fun _ -> randomInRange range, randomInRange range)
        |> Seq.map (op)
        |> Seq.find (Option.isSome << Option.bind (optionInRange) << calc)

I've created a Range record to contain the range data I'm going to be working with.

My randomInRange function generates a random number within the specified range.

My isInRange function determines whether a given value is within the supplied range.

My randomOpGen function generates a number in the range of 0-3 and then generates a random type constructor for Problem: Addition when the random value is 1, Subtraction when 2, etc.

(You might wonder why I've defined this function with a unit argument rather than just accepting the tuple, the answer is so that I can get it to generate operators with equal likelihood later.)

My calc function resolves the arithmetic by performing the appropriate operation. I've modified the result of this function so that it handles integer division by returning Some result for cases where the remainder is 0 and None otherwise. All the other computations always return Some result.


quiz is where the magic happens.

First I generate a random operator, this will be the same for every element in the sequence later - hence the importance of the () I mentioned earlier.

I generate an infinite sequence of integer tuples from the supplied range and, using map, generate an operation (the one I created earlier) on each of these tuples.

I then use Seq.find to find the first occurrence of a result that's both within the range that I specified and has a valid result value.


Now let's try this code:

let x = NumeracyProblems.quiz {Min = 1; Max = 9}
x
NumeracyProblems.calc x;;
val x : Problem = Addition (2,7) 
val it : int option = Some 9

Now let's change the range

let x = NumeracyProblems.quiz {Min = 1; Max = 99}
x
NumeracyProblems.calc x
val x : Problem = Division (56,2)
val it : int option = Some 28

As you can see, this version of the code is completely agnostic to the integer range.

Upvotes: 1

Related Questions