0

I am attempting to refactor a function (found towards the end of this StackOverflow answer) to make it slightly more generic. Here's the original function definition:

def tryProcessSource(
  file: File,
  parseLine: (Int, String) => Option[List[String]] =
    (index, unparsedLine) => Some(List(unparsedLine)),
  filterLine: (Int, List[String]) => Option[Boolean] =
    (index, parsedValues) => Some(true),
  retainValues: (Int, List[String]) => Option[List[String]] =
    (index, parsedValues) => Some(parsedValues)
): Try[List[List[String]]] = {
  ???
}

And here is to what I am attempting to change it:

def tryProcessSource[A <: Any](
  file: File,
  parseLine: (Int, String) => Option[List[String]] =
    (index, unparsedLine) => Some(List(unparsedLine)),
  filterLine: (Int, List[String]) => Option[Boolean] =
    (index, parsedValues) => Some(true),
  transformLine: (Int, List[String]) => Option[A] =
    (index, parsedValues) => Some(parsedValues)
): Try[List[A]] = {
  ???
}

I keep getting a highlight error in the IntelliJ editor on Some(parsedValues) which says, "Expression of type Some[List[String]] doesn't conform to expected type Option[A]". I've been unable to figure out how to properly modify the function definition to satisfy the required condition; i.e. so the error goes away.

If I change transformLine to this (replace the generic parameter A with Any)...

transformLine: (Int, List[String]) => Option[Any] =
  (index, parsedValues) => Some(parsedValues)

...the error goes away. But, I also lose the strong typing associated with the generic parameter.

Any assistance on with this is very much appreciated.

Community
  • 1
  • 1
chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86
  • Well, how can you convert a `List[String]` into an arbitrary `A`? It doesn't really make sense, unless you have some evidence to convert a `List[String]` to an `A` already. Some serious refactoring using type-classes could work, but I don't think it makes a lot of sense to have the default parameter anymore. – Michael Zajac Dec 15 '15 at 16:30
  • @m-z I now think I understand what you are saying. Please see the answer I just posted and let me know if you seen anything wrong or could be improved. Thank you. – chaotic3quilibrium Dec 15 '15 at 19:05

2 Answers2

1

In transformLine: (Int, List[String]) => Option[A] = (index, parsedValues) => whatever, parsedValues obviously has type List[String], and so Some(parsedValues) is Some[List[String]]. There is simply no reasonable default value for transformLine.

You can change its type to (Int, A) => Option[A] or (Int, List[A]) => Option[List[A]] depending on what you need, and change the previous arguments as well, but you'll get stuck on the fact that lines are Strings, so you'll have to convert them to A somewhere.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • I don't understand your answer. The point of `transformLine` is to take the index and the parsed string values from a specific line and convert them to an optional arbitrary type. Converting is the whole point of the `transformLine` function. And since the function can return `Option[+Any]`, why doesn't `Some[List[String]]` conform to `Option[+Any]`. IOW, if I change it explicitly to `Option[Any]` (as opposed to Option[A]), then the error disappears and `Some(parsedValue)` works fine. – chaotic3quilibrium Dec 15 '15 at 17:24
  • 2
    @chaotic3quilibrium - Your default `transformLine` doesn't transform the line to an arbitrary type, it only works for `String`, which is why you get the error. An `Option[Any]` is different to an `Option[A]` since the client chooses `A` while the function can easily choose an `Any`. – Lee Dec 15 '15 at 17:52
  • @Alexey Romanov Okay, I think I now understand what you are saying. I've now created an answer to my question which more explicitly and concretely expresses what you are implying. – chaotic3quilibrium Dec 15 '15 at 18:40
  • 1
    @chaotic3quilibrium Suppose someone calls `tryProcessSource[Int](file)` (leaving the other arguments as default). Then `transformLine` needs to have type `(Int, List[String]) => Option[Int]`, which your default value doesn't. – Alexey Romanov Dec 15 '15 at 18:43
  • I now think I got it. Please read the answer I just posted to make sure I really did get it. And any feedback you can give me on the guideline/principle I highlighted is greatly appreciated. – chaotic3quilibrium Dec 15 '15 at 18:46
0

After reading and re-reading Alexey Romonov's answer and Lee's comment, it gave me a clue into how to refactor this to get it working. Additionally, I think it might also implies a guideline regarding providing default values to a function.

If I am understand it correctly, I am trying to combine a concrete, in this example case List[String] in the transform parameter default, with a generic type, in this case Option[A]. Essentially, this overlaps the boundary upon which a generic copy of the function is instantiated by the compiler for the specific user provided type and the explicitly provided List[String]. This leads me to the following guideline regarding defining generic functions:

When transforming a concrete function to make it generic, each parameter with a default value which interacts directly with any of the generic parameters likely needs to be removed and placed into an enclosing concrete function which forwards the call to the generic function.


So with this guideline in mind, let's rework the original code...

The concrete function looks like this (very similar to the original pre-generic-ified version which also included the desired concrete default parameters):

def tryProcessSource(
  file: File,
  parseLine: (Int, String) => Option[List[String]] =
    (index, unparsedLine) => Some(List(unparsedLine)),
  filter: (Int, List[String]) => Option[Boolean] =
    (index, parsedValues) => Some(true),
  transform: (Int, List[String]) => Option[List[String]] =
    (index, parsedValues) => Some(parsedValues)
): Try[List[List[String]]] =
  tryProcessSourceGeneric(file, parseLine, filter, transform)

And the generic-ified function looks like this (notice the lack of any parameter default values):

def tryProcessSourceGeneric[A, B](
  file: File,
  parseLine: (Int, String) => Option[A],
  filter: (Int, A) => Option[Boolean]
  transform: (Int, A) => Option[B]
): Try[List[B]] = {
  ???
}

Again, a huge thank you to Alexey and Lee for helping me struggle through this.

chaotic3quilibrium
  • 5,661
  • 8
  • 53
  • 86
  • 1
    It can make sense for parameters involving generics to have default values. In your example, you could still have the same default value for `filterLine`, because it works for any `A`. Or if you remove the `B` parameter and have `transform: (Int, A) => Option[A]`, then `(_, x) => Some(x)` is a reasonable default value. – Alexey Romanov Dec 15 '15 at 18:50
  • Awesome! So my guideline holds for `filter` and my existing `transform`. However, it doesn't hold for your `transform` example. Hmmm...not sure how to reword it. Mulling... – chaotic3quilibrium Dec 15 '15 at 19:04