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 :).

asked Dec 2, 2018 at 10:40
\$\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
answered Jan 30, 2019 at 11:54
\$\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\$ 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.

answered Dec 2, 2018 at 20:57
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.