如何在程序以不可变语言进入实例时添加新信息

时间:2017-05-30 06:00:30

标签: scala functional-programming

我一直很难在Scala中完成任务,这在像Java这样的可变世界中很容易做到。

我有List个实例。 (让我们称他们ListItem,或List[Item]}。随着程序的继续,我们会获得有关每个Item的更多信息。 (让我们调用这些类ItemAttributeAItemAttributeB等等......)为了不忘记哪个ItemAttributeA对应哪个Item,我是'我希望将它们保持在同一个List List[(Item, ItemAttributeA, ItemAttributeB)]

虽然这个元组列表已经看起来非常难看,但如果我有几个案例类,它会变得更加糟糕。 (如List[(Item, ItemAttributeA, ItemAttributeB, ItemAttributeC, ItemAttributeD, ItemAttributeE, ItemAttributeF, ...)]

在一个可变的世界中,我们可以编写如下的清洁代码。

case class Item(id: Int) {
  var attrA: ItemAttributeA = null //
  var attrB: ItemAttributeB = null // fill these fields later
}
val getItemAttributeA: List[Item] => List[ItemAttributeA]
val getItemAttributeB: List[Item] => List[ItemAttributeB]

val items = List(Item(1))
items.zip(getItemAttributeA(items)).foreach { case (item, itemAttrA) =>
  item.attrA = itemAttrA
}
items.zip(getItemAttributeB(items)).foreach { case (item, itemAttrB) =>
  item.attrB = itemAttrB
}

如果您使用Optioncopy,也许您不需要可变字段,但它有点混乱。而且,它仍有一些问题。

case class Item(
  id: Int,
  attrA: Option[ItemAttributeA],
  attrB: Option[ItemAttributeB]
)

val getItemAttributeA: List[Item] => List[ItemAttributeA] // I get list of information at once for a performance reason.
val getItemAttributeB: List[Item] => List[ItemAttributeB]

val items = List(Item(1, None, None))
val itemsWithA = items.zip(getItemAttributeA(items)).map { case (item, itemAttrA) =>
  item.copy(attrA = Some(itemAttrA))
}
val itemsWithAandB = itemsWithA.zip(getItemAttributeB(itemsWithA)).map { case (item, itemAttrB) =>
  item.copy(attrB = Some(itemAttrB))
}

// All item, itemWithAttrA, itemWithAttrB have same type Item. 
// This doesn't sound good because we can't know which instance have the information we want by just looking at the type.

我目前最好的解决方案是使用只有一个案例类作为字段的特征。

trait HasItem { val item: Item }
trait HasItemAttributeA { val itemAttrA: ItemAttributeA }
trait HasItemAttributeB { val itemAttrB: ItemAttributeB }

val getItemAttributeA: List[HasItem] => List[HasItem with HasItemAttributeA]
val getItemAttributeB: List[HasItem with HasItemAttributeA] => List[HasItem with HasItemAttributeA with HasItemAttributeB]

val hasItem = new HasItem { val item = Item(1) }
val hasItemWithA = getItemWithA(hasItem)
val hasItemWithAandB = getItemAttributeB(hasItemWithA)

嗯,它看起来并不奇怪。但至少它满足了一些你无法满足元组的需求。例如,这样的案例。

// you want to add ItemAttributeX to each Item in the list.
// but you only need Item and ItemAttributeB to get ItemAttributeX
// you can express that by using type parameters
def getItemAttributeX[I < HasItem with HasItemAttributeB](listI: List[I]): List[I with HasItemAttributeX]

val itemsWithManyAttributes: List[HasItem with HasItemAttributeA with HasItemAttributeB with HasItemAttributeC with HasItemAttributeD with HasItemAttributeE]
val itemsWithManyAttributesAndX = getItemAttributeX(itemsWithManyAttributes)

即使这种方式有所作为,但还有很多其他问题。 (可读性,很多样板,多次创建新实例等等)

我的问题是,希望以功能性方式解决这些问题的最佳方法是什么?

1 个答案:

答案 0 :(得分:1)

我认为在这种情况下,可变性是可以的(-ish),但我建议使用经典的构建器模式,使用可变类来收集所有信息。收集所有信息后,您将从该构建器生成不可变对象(包括您确实拥有所需信息的测试)。请注意,Scala API本身就是这样做的(例如,填写ListBuffer,并在完成后创建一个不可变的List。如果你想要花哨,甚至有type safe builder pattern

尽管如此,有一些纯粹的功能性方法来处理这种情况,例如:作家monad。但是,Scala不是Haskell,所以你可以(而且应该)在更容易的情况下先熟悉函数编程,然后再拔出大枪。