3
\$\begingroup\$

I am using the coding puzzels of https://adventofcode.com for improving my F# skills.

Problem:

Day 1 Part 2:

Starting with frequency 0, a list of numbers should be added successive generating new frequencies. Searched is the first frequency which occurs twice. If the end of the list has been reached without success; the list should be processed again.

Example Input: [+1, -2, +3, +1]

  • Current frequency 0, change of +1; resulting frequency 1.
  • Current frequency 1, change of -2; resulting frequency -1. C* urrent frequency -1, change of +3; resulting frequency 2.
  • Current frequency 2, change of +1; resulting frequency 3. (At this point, we continue from the start of the list.)
  • Current frequency 3, change of +1; resulting frequency 4.
  • Current frequency 4, change of -2; resulting frequency 2, which has already been seen.

In this example, the first frequency reached twice is 2.

Solution

let input = [1;-2;3;1]

type State =
    | FinalState of int
    | IntermediateState of int * int Set

type State with
    static member Zero = IntermediateState(0, Set.empty.Add(0)) 

let foldIntermediateState (freqHistory: int Set, freq : int) : State =
    let isFinal = freqHistory |> Set.contains freq
    if isFinal then
        FinalState (freq)
    else
        IntermediateState (freq, freqHistory.Add(freq))

let foldState state value =
    match state with
        | FinalState _ -> state
        | IntermediateState (lastFreq, freqHistory) -> foldIntermediateState (freqHistory, lastFreq + value)

let rec processListRec state lst =
    let result = lst |> Seq.fold foldState state
    match result with
        | FinalState result -> printfn "The result is: %i" result
        | IntermediateState _ -> lst |> processListRec result

input |> Seq.map int |> processListRec State.Zero

Questions

In fact, I am proud to have found a solution without mutable states that looks (at least for me) functional! :D. So any feeback that helps improving the solution or even alternative function approches for solving that problem are welcome :).

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

It also looks functional to me, and I like your use of union type State.


You can define the Zero static member of State as:

type State =
    | FinalState of int
    | IntermediateState of int * int Set
    static member Zero = IntermediateState(0, Set.empty.Add(0)) 

You don't have to create a separate with-definition.


In general I avoid writing the result to the console in the algorithm function it self. Instead it is more useful to return the result and let the caller handle that as needed:

let rec processListRec state lst =
    let result = lst |> Seq.fold foldState state
    match result with
    | FinalState result -> result
    | IntermediateState _ -> lst |> processListRec result

Used as:

printfn "%A" (processListRec State.Zero input)

You need some kind of stop condition or an input check if all changes are positive or negative.

For instance will [1;1;1] run forever, because no frequency will ever occur twice.


A raw recursive solution could be:

let findReoccurantFrequency input = 
    if (input |> List.groupBy (fun x -> x > 0)) |> List.length < 2 then 
        raise (Exception "Invalid input (only positive or negative changes)")
    else
        let rec runner lst freq freqs =
            if freqs |> List.contains freq then
                freq
            else
                let head, tail = if lst = [] then input.Head, input.Tail else lst.Head, lst.Tail
                runner tail (freq + head) (freq::freqs)
        runner input 0 []

I'm not saying that it's a better solution, just another approach for the exercise and inspiration. Especially I'm not proud of the input check.


Edit

As pointed out by JanDotNet in his comment, my input validation is poor, so it's maybe better to leave it out. Below is a revised version of my implementation using a Set instead of a List for the frequencies:

let findReoccurantFrequency input = 
    let rec runner lst freq freqs =
        match freqs |> Set.contains freq with
        | true -> freq
        | false ->
            let head, tail = if lst = [] then input |> List.head, input |> List.tail else lst.Head, lst.Tail
            runner tail (freq + head) (freqs.Add(freq))
    runner input 0 Set.empty
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks for your answer. The points are absolutly valid :). 2 small comments: a) Determining invalid input is not such easy (e.g. [3;-1] will also result in an endless loop) Therefore I would assume that the input is valid. b) I really like your recursive solution! It is much cleaner and less code :). Note that for the real problem statement (input of 1015 numbers which create a list of 145.146 freqs) it more appropriated to use a Set<int> instead of a list<int> for the freqs. \$\endgroup\$
    – JanDotNet
    Commented Jan 30, 2019 at 20:01
1
\$\begingroup\$

Caveat : I don't know F#.

I liked your solution, the use of algebraic data types, pattern matching and use of list processing via fold and higher order functions are good.

However I'm trying to apply a functional style in python (which I'm learning) and I think there is a way to do this that use simple list data structures, with (list) comprehension or the equivalent

Here is my attempt in python to present an alternative approach

import functools
import itertools

fchanges = []
with open('frequency_input') as f:
    for line in f:
            fchanges.append(int(line))

frequencies = [sum(fchanges[:index]) for index in range(1, len(fchanges)+1)]
skew = frequencies[-1]

print('Skew is ' + str(skew))
accumulator = itertools.accumulate(itertools.cycle(fchanges))
prefix = itertools.takewhile(lambda x: (x + skew) not in frequencies, accumulator)

# takewhile consumes the value I'm looking for to check the predicate, hence this hack
# don't need the list, memory optimization - just need the length
plen = functools.reduce(lambda x, y: x+1, prefix, 0)
accumulator = itertools.accumulate(itertools.cycle(fchanges))
print('found first repetition ' + str(next(itertools.islice(accumulator, plen, None)) + skew))

Happy to receive feedback on my attempt too for any observers.

\$\endgroup\$

Your Answer

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

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