Reputation: 271
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
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