From 8bd589127fb28ef567110e289ec78edf4529e1ab Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Mon, 30 Apr 2018 14:37:20 +0300 Subject: [PATCH 1/7] Added State monad from cats. --- build.sbt | 4 +- .../matching/HouseMarketSpecification.scala | 2 +- .../MarriageMarketSpecification.scala | 2 +- .../matching/DeferredAcceptance.scala | 127 +++++++++--------- .../DeferredAcceptanceMicroBenchmark.scala | 2 +- .../matching/WeaklyStableMatchingSpec.scala | 6 +- 6 files changed, 74 insertions(+), 69 deletions(-) diff --git a/build.sbt b/build.sbt index abbc298..6375a5e 100644 --- a/build.sbt +++ b/build.sbt @@ -8,6 +8,7 @@ lazy val commonSettings = Seq( organizationHomepage := Some(url("https://economicsl.github.io/")), libraryDependencies ++= Seq( "org.scalactic" %% "scalactic" % "3.0.5", + "org.typelevel" %% "cats-core" % "1.1.0", "com.typesafe.akka" %% "akka-actor" % "2.5.6", "org.economicsl" %% "esl-core" % "0.1.0-SNAPSHOT" ), @@ -22,7 +23,8 @@ lazy val commonSettings = Seq( "-language:reflectiveCalls", // needed in order to enable structural (or duck) typing "-Xlint", "-Ywarn-unused-import", - "-Ywarn-dead-code" + "-Ywarn-dead-code", + "-Ypartial-unification" ) ) diff --git a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala index 2dcf01d..7839589 100644 --- a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala +++ b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala @@ -66,7 +66,7 @@ object HouseMarketSpecification extends Properties("housing-market") { property("incentive-compatibility") = forAll(housePurchaseOffers, houseListings) { case (offers, listings) => - val (_, _, matches) = DeferredAcceptance.weaklyStableMatching2(offers, listings) + val ((_, _), matches) = DeferredAcceptance.weaklyStableMatching2[HousePurchaseOffer, HouseListing].run((offers, listings)).value matches.forall{ case (offer, listing) => offer.price >= listing.price } } diff --git a/src/functional/scala/org/economicsl/matching/MarriageMarketSpecification.scala b/src/functional/scala/org/economicsl/matching/MarriageMarketSpecification.scala index d5de638..25c1c12 100644 --- a/src/functional/scala/org/economicsl/matching/MarriageMarketSpecification.scala +++ b/src/functional/scala/org/economicsl/matching/MarriageMarketSpecification.scala @@ -47,7 +47,7 @@ object MarriageMarketSpecification extends Properties("marriage-market") { property("all men and women are matched") = forAll(unMatched) { case (ms, ws) => - val (unMatchedMs, unMatchedWs, matching) = DeferredAcceptance.weaklyStableMatching(ms, ws) + val ((unMatchedMs, unMatchedWs), matching) = DeferredAcceptance.weaklyStableMatching[Man, Woman].run((ms, ws)).value unMatchedMs.isEmpty && unMatchedWs.isEmpty && (matching.size == ms.size) } diff --git a/src/main/scala/org/economicsl/matching/DeferredAcceptance.scala b/src/main/scala/org/economicsl/matching/DeferredAcceptance.scala index 1402424..3124149 100644 --- a/src/main/scala/org/economicsl/matching/DeferredAcceptance.scala +++ b/src/main/scala/org/economicsl/matching/DeferredAcceptance.scala @@ -15,7 +15,9 @@ limitations under the License. */ package org.economicsl.matching +import cats.data.State import org.scalactic.TripleEquals._ + import scala.collection.immutable.{HashMap, HashSet} @@ -84,8 +86,6 @@ object DeferredAcceptance { /** Compute a weakly stable matching between two sets of equal size. * - * @param ms set of proposers to be matched. - * @param ws set of proposees to be matched. * @tparam M the type of proposer. * @tparam W the type of proposee. * @return a weakly stable matching between proposees (`ws`) and proposers (`ms`). @@ -96,39 +96,40 @@ object DeferredAcceptance { * From Dubins and Freedman (1981) and Roth (1982) it is a weakly dominant strategy for a type `M` agent to * state its true preferences. */ - def weaklyStableMatching[M <: Proposer with Preferences[W], W <: Preferences[M]](ms: Set[M], ws: Set[W]): (Set[M], Set[W], Map[W, M]) = { - require(ms.size === ws.size) + def weaklyStableMatching[M <: Proposer with Preferences[W], W <: Preferences[M]]: State[(Set[M], Set[W]), Map[W, M]] = { - @annotation.tailrec - def accumulate(unMatchedMs: Set[M], matched: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = { - unMatchedMs.headOption match { - case Some(unMatchedM) => - val previouslyRejected = rejected.getOrElse(unMatchedM, Set.empty) - val mostPreferredW = ws.diff(previouslyRejected).max(unMatchedM.ordering) - matched.get(mostPreferredW) match { - case Some(m) if mostPreferredW.ordering.lt(m, unMatchedM) => // mostPreferredW has received strictly better offer! - val updatedUnMatchedMs = unMatchedMs - unMatchedM + m - val updatedMatched = matched.updated(mostPreferredW, unMatchedM) - val updatedRejected = rejected.updated(m, rejected.getOrElse(m, Set.empty) + mostPreferredW) - accumulate(updatedUnMatchedMs, updatedMatched, updatedRejected) - case Some(m) if mostPreferredW.ordering.gteq(m, unMatchedM) => // mostPreferredW already has weakly better offer! - val updatedRejected = previouslyRejected + mostPreferredW - accumulate(unMatchedMs, matched, rejected.updated(unMatchedM, updatedRejected)) - case None => // mostPreferredW has yet to receive any offer! - accumulate(unMatchedMs - unMatchedM, matched + (mostPreferredW -> unMatchedM), rejected) - } - case None => - assert(unMatchedMs.isEmpty) - (unMatchedMs, ws.diff(matched.keySet), matched) + State { case (ms, ws) => + require(ms.size === ws.size) + + @annotation.tailrec + def accumulate(unMatchedMs: Set[M], matched: Map[W, M], rejected: Map[M, Set[W]]): ((Set[M], Set[W]), Map[W, M]) = { + unMatchedMs.headOption match { + case Some(unMatchedM) => + val previouslyRejected = rejected.getOrElse(unMatchedM, Set.empty) + val mostPreferredW = ws.diff(previouslyRejected).max(unMatchedM.ordering) + matched.get(mostPreferredW) match { + case Some(m) if mostPreferredW.ordering.lt(m, unMatchedM) => // mostPreferredW has received strictly better offer! + val updatedUnMatchedMs = unMatchedMs - unMatchedM + m + val updatedMatched = matched.updated(mostPreferredW, unMatchedM) + val updatedRejected = rejected.updated(m, rejected.getOrElse(m, Set.empty) + mostPreferredW) + accumulate(updatedUnMatchedMs, updatedMatched, updatedRejected) + case Some(m) if mostPreferredW.ordering.gteq(m, unMatchedM) => // mostPreferredW already has weakly better offer! + val updatedRejected = previouslyRejected + mostPreferredW + accumulate(unMatchedMs, matched, rejected.updated(unMatchedM, updatedRejected)) + case None => // mostPreferredW has yet to receive any offer! + accumulate(unMatchedMs - unMatchedM, matched + (mostPreferredW -> unMatchedM), rejected) + } + case None => + assert(unMatchedMs.isEmpty) + ((unMatchedMs, ws.diff(matched.keySet)), matched) + } } + accumulate(ms, Map.empty, Map.empty) } - accumulate(ms, Map.empty, Map.empty) } /** Compute a weakly stable matching between two sets. * - * @param ms set of proposers to be matched. - * @param ws set of proposees to be matched. * @tparam M the type of proposer. * @tparam W the type of proposee. * @return @@ -136,43 +137,45 @@ object DeferredAcceptance { * to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n` * is the size of the inputs sets. */ - def weaklyStableMatching2[M <: Proposer with Predicate[W] with Preferences[W], - W <: Predicate[M] with Preferences[M]] - (ms: Set[M], ws: Set[W]) - : (Set[M], Set[W], Map[W, M]) = { - - @annotation.tailrec - def accumulate(unMatchedMs: Set[M], toBeMatchedMs: Set[M], matched: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = { - toBeMatchedMs.headOption match { - case Some(toBeMatchedM) => - val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty) - val acceptableWs = ws.diff(previouslyRejected) - if (acceptableWs.isEmpty) { - accumulate(unMatchedMs + toBeMatchedM, toBeMatchedMs - toBeMatchedM, matched, rejected) - } else { - val mostPreferredW = acceptableWs.max(toBeMatchedM.ordering) - matched.get(mostPreferredW) match { - case Some(m) if mostPreferredW.ordering.lt(m, toBeMatchedM) => // mostPreferredW has received strictly better offer! - val updatedToBeMatchedMs = toBeMatchedMs - toBeMatchedM + m - val updatedMatched = matched.updated(mostPreferredW, toBeMatchedM) - val updatedRejected = rejected.updated(m, rejected.getOrElse(m, Set.empty) + mostPreferredW) - accumulate(unMatchedMs, updatedToBeMatchedMs, updatedMatched, updatedRejected) - case Some(m) if mostPreferredW.ordering.gteq(m, toBeMatchedM) => // mostPreferredW already has weakly better offer! - val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) - accumulate(unMatchedMs, toBeMatchedMs, matched, updatedRejected) - case None if mostPreferredW.isAcceptable(toBeMatchedM) => // mostPreferredW has yet to receive an acceptable offer! - accumulate(unMatchedMs, toBeMatchedMs - toBeMatchedM, matched + (mostPreferredW -> toBeMatchedM), rejected) - case None => // unMatchedM proposal is not acceptable to mostPreferredW! - val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) - accumulate(unMatchedMs, toBeMatchedMs, matched, updatedRejected) + def weaklyStableMatching2[M <: Proposer with Predicate[W] with Preferences[W], W <: Predicate[M] with Preferences[M]] + : State[(Set[M], Set[W]), Map[W, M]] = { + + State { case (ms, ws) => + + @annotation.tailrec + def accumulate(unMatchedMs: Set[M], toBeMatchedMs: Set[M], matched: Map[W, M], rejected: Map[M, Set[W]]): ((Set[M], Set[W]), Map[W, M]) = { + toBeMatchedMs.headOption match { + case Some(toBeMatchedM) => + val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty) + val acceptableWs = ws.diff(previouslyRejected) + if (acceptableWs.isEmpty) { + accumulate(unMatchedMs + toBeMatchedM, toBeMatchedMs - toBeMatchedM, matched, rejected) + } else { + val mostPreferredW = acceptableWs.max(toBeMatchedM.ordering) + matched.get(mostPreferredW) match { + case Some(m) if mostPreferredW.ordering.lt(m, toBeMatchedM) => // mostPreferredW has received strictly better offer! + val updatedToBeMatchedMs = toBeMatchedMs - toBeMatchedM + m + val updatedMatched = matched.updated(mostPreferredW, toBeMatchedM) + val updatedRejected = rejected.updated(m, rejected.getOrElse(m, Set.empty) + mostPreferredW) + accumulate(unMatchedMs, updatedToBeMatchedMs, updatedMatched, updatedRejected) + case Some(m) if mostPreferredW.ordering.gteq(m, toBeMatchedM) => // mostPreferredW already has weakly better offer! + val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) + accumulate(unMatchedMs, toBeMatchedMs, matched, updatedRejected) + case None if mostPreferredW.isAcceptable(toBeMatchedM) => // mostPreferredW has yet to receive an acceptable offer! + accumulate(unMatchedMs, toBeMatchedMs - toBeMatchedM, matched + (mostPreferredW -> toBeMatchedM), rejected) + case None => // unMatchedM proposal is not acceptable to mostPreferredW! + val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) + accumulate(unMatchedMs, toBeMatchedMs, matched, updatedRejected) + } } - } - case None => - (unMatchedMs, matched.keySet.diff(ws), matched) + case None => + ((unMatchedMs, matched.keySet.diff(ws)), matched) + } } + + val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) + accumulate(Set.empty, ms, Map.empty, unacceptableWs) } - val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) - accumulate(Set.empty, ms, Map.empty, unacceptableWs) } } diff --git a/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala b/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala index 572ab85..fe7d9e2 100644 --- a/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala +++ b/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala @@ -64,7 +64,7 @@ object DeferredAcceptanceMicroBenchmark extends Bench.OnlineRegressionReport { measure method "weaklyStableMatching" in { using(unMatchedParticipants) in { case (buyers, sellers) => - DeferredAcceptance.weaklyStableMatching(buyers, sellers) + DeferredAcceptance.weaklyStableMatching.run(buyers, sellers) } } diff --git a/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala b/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala index 771ed27..811022d 100644 --- a/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala +++ b/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala @@ -21,17 +21,17 @@ import org.scalatest.FlatSpec class WeaklyStableMatchingSpec extends FlatSpec { "A weakly stable matching of two empty sets" should "be an empty map" in { - val (_, _, matching) = DeferredAcceptance.weaklyStableMatching(Set.empty[Man], Set.empty[Woman]) + val ((_, _), matching) = DeferredAcceptance.weaklyStableMatching[Man, Woman].run((Set.empty, Set.empty)).value assert(matching.isEmpty) } it should "throw IllegalArgumentException when attempting to match sets of different size." in { assertThrows[IllegalArgumentException] { - DeferredAcceptance.weaklyStableMatching(Set(Man(1, 42, Man.womanByQuality)), Set.empty[Woman]) + DeferredAcceptance.weaklyStableMatching[Man, Woman].run((Set(Man(1, 42, Man.womanByQuality)), Set.empty[Woman])).value } assertThrows[IllegalArgumentException] { - DeferredAcceptance.weaklyStableMatching(Set.empty[Man], Set(Woman(1, 42, Woman.manByQuality))) + DeferredAcceptance.weaklyStableMatching[Man, Woman].run((Set.empty[Man], Set(Woman(1, 42, Woman.manByQuality)))).value } } From 602eba67d12681e17c4808653b0bf9dd7a91f3b4 Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Thu, 3 May 2018 14:06:55 +0300 Subject: [PATCH 2/7] Now wrapping matching algorithms in State monads. --- .../economicsl/matching/HouseListing.scala | 2 +- .../matching/HouseMarketSpecification.scala | 63 +++++++++---------- .../matching/HousePurchaseOffer.scala | 2 +- .../DeferredAcceptanceAlgorithm.scala | 15 +++-- .../matching/StableMarriageAlgorithm.scala | 15 +++-- .../DeferredAcceptanceMicroBenchmark.scala | 8 +-- .../MarriageMarketSpecification.scala | 17 ++--- .../matching/WeaklyStableMatchingSpec.scala | 6 +- 8 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/functional/scala/org/economicsl/matching/HouseListing.scala b/src/functional/scala/org/economicsl/matching/HouseListing.scala index fd2f1c5..4c79af2 100644 --- a/src/functional/scala/org/economicsl/matching/HouseListing.scala +++ b/src/functional/scala/org/economicsl/matching/HouseListing.scala @@ -20,7 +20,7 @@ import java.util.UUID import org.economicsl.core.Price -case class HouseListing(uuid: UUID, issuer: Long, price: Price, house: House) +case class HouseListing(uuid: UUID, issuer: UUID, price: Price, house: House) extends Predicate[HousePurchaseOffer] with Preferences[HousePurchaseOffer] { /** Boolean function used to determine whether some `HousePurchaseOffer` is acceptable. diff --git a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala index ba62ec6..c334391 100644 --- a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala +++ b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala @@ -15,65 +15,64 @@ limitations under the License. */ package org.economicsl.matching +import cats.data.State import org.economicsl.core.Price import org.scalacheck.{Gen, Properties} -import scala.collection.immutable.HashSet - object HouseMarketSpecification extends Properties("housing-market") { import org.scalacheck.Prop._ // this generator should exist in esl-core! - val priceGen: Gen[Price] = { + def price: Gen[Price] = { for { - value <- Gen.choose(100000, 1000000) + value <- Gen.posNum[Long] } yield Price(value) } - val houseGen: Gen[House] = { + val house: Gen[House] = { for { - id <- Gen.uuid - quality <- Gen.choose(1, 100) - } yield House(id, quality) + uuid <- Gen.uuid + quality <- Gen.posNum[Int] + } yield House(uuid, quality) } - val houseListingGen: Gen[HouseListing] = { + val houseListing: Gen[HouseListing] = { for { - id <- Gen.uuid - issuer <- Gen.posNum[Long] - price <- priceGen - house <- houseGen - } yield HouseListing(id, issuer, price, house) + uuid <- Gen.uuid + issuer <- Gen.uuid + price <- price + house <- house + } yield HouseListing(uuid, issuer, price, house) } - val housePurchaseOfferGen: Gen[HousePurchaseOffer] = { + val housePurchaseOffer: Gen[HousePurchaseOffer] = { for { - id <- Gen.uuid - issuer <- Gen.posNum[Long] - price <- priceGen - } yield HousePurchaseOffer(id, issuer, price) + uuid <- Gen.uuid + issuer <- Gen.uuid + price <- price + } yield HousePurchaseOffer(uuid, issuer, price) } - val housePurchaseOffers: Gen[HashSet[HousePurchaseOffer]] = Gen.sized { - n => Gen.containerOfN[HashSet, HousePurchaseOffer](n, housePurchaseOfferGen) - } + def unMatched: Gen[(Set[HousePurchaseOffer], Set[HouseListing])] = Gen.sized { + size => for { + offers <- Gen.containerOfN[Set, HousePurchaseOffer](size, housePurchaseOffer) + listing <- Gen.containerOfN[Set, HouseListing](size, houseListing) + } yield (offers, listing) - val houseListings: Gen[HashSet[HouseListing]] = Gen.sized { - n => Gen.containerOfN[HashSet, HouseListing](n, houseListingGen) } - property("matching should be incentive-compatible") = forAll(housePurchaseOffers, houseListings) { - case (offers, listings) => - val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing])(offers, listings) - matching.matches.forall{ case (offer, listing) => offer.price >= listing.price } + property("matching should be incentive-compatible") = forAll(unMatched) { unmatched => + val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched) + val ((_, _), matching) = result.value + matching.matches.forall{ case (offer, listing) => offer.price >= listing.price } } - property("matching should be stable") = forAll(housePurchaseOffers, houseListings) { - case (offers, listings) => - val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing])(offers, listings) - offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o))) + property("matching should be stable") = forAll(unMatched) { unmatched => + val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched) + val ((offers, listings), matching) = result.value + offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o))) } } diff --git a/src/functional/scala/org/economicsl/matching/HousePurchaseOffer.scala b/src/functional/scala/org/economicsl/matching/HousePurchaseOffer.scala index 1810c35..e927b02 100644 --- a/src/functional/scala/org/economicsl/matching/HousePurchaseOffer.scala +++ b/src/functional/scala/org/economicsl/matching/HousePurchaseOffer.scala @@ -20,7 +20,7 @@ import java.util.UUID import org.economicsl.core.Price -case class HousePurchaseOffer(uuid: UUID, issuer: Long, price: Price) +case class HousePurchaseOffer(uuid: UUID, issuer: UUID, price: Price) extends Proposer with Predicate[HouseListing] with Preferences[HouseListing] { /** Boolean function used to determine whether some `HouseListing` is acceptable. diff --git a/src/main/scala/org/economicsl/matching/DeferredAcceptanceAlgorithm.scala b/src/main/scala/org/economicsl/matching/DeferredAcceptanceAlgorithm.scala index fa71108..51a959d 100644 --- a/src/main/scala/org/economicsl/matching/DeferredAcceptanceAlgorithm.scala +++ b/src/main/scala/org/economicsl/matching/DeferredAcceptanceAlgorithm.scala @@ -22,25 +22,24 @@ package org.economicsl.matching * @tparam W the type of proposee. */ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferences[W], W <: Predicate[M] with Preferences[M]] - extends ((Set[M], Set[W]) => ((Set[M], Set[W]), Matching[W, M])) { + extends (((Set[M], Set[W])) => ((Set[M], Set[W]), Matching[W, M])) { /** Compute a weakly stable matching between two sets. * - * @param ms set of proposers to be matched. - * @param ws set of proposees to be matched. + * @param unmatched * @return * @note A matching will be called "weakly stable" unless there is a pair each of which strictly prefers the other * to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n` * is the size of the inputs sets. */ - def apply(ms: Set[M], ws: Set[W]): ((Set[M], Set[W]), Matching[W, M]) = { + def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), Matching[W, M]) = { @annotation.tailrec def accumulate(unMatchedMs: Set[M], toBeMatchedMs: Set[M], matches: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = { toBeMatchedMs.headOption match { case Some(toBeMatchedM) => val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty) - val acceptableWs = ws.diff(previouslyRejected) + val acceptableWs = unmatched._2.diff(previouslyRejected) if (acceptableWs.isEmpty) { accumulate(unMatchedMs + toBeMatchedM, toBeMatchedMs - toBeMatchedM, matches, rejected) } else { @@ -63,11 +62,11 @@ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferenc } } case None => - (unMatchedMs, matches.keySet.diff(ws), matches) + (unMatchedMs, matches.keySet.diff(unmatched._2), matches) } } - val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) - val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs) + val unacceptableWs = unmatched._1.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> unmatched._2.filter(m.isAcceptable))) + val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, unmatched._1, Map.empty, unacceptableWs) ((unMatchedMs, unMatchedWs), Matching(matches)) } diff --git a/src/main/scala/org/economicsl/matching/StableMarriageAlgorithm.scala b/src/main/scala/org/economicsl/matching/StableMarriageAlgorithm.scala index 410311b..1984d99 100644 --- a/src/main/scala/org/economicsl/matching/StableMarriageAlgorithm.scala +++ b/src/main/scala/org/economicsl/matching/StableMarriageAlgorithm.scala @@ -36,23 +36,22 @@ import org.scalactic.TripleEquals._ * [[http://www.eecs.harvard.edu/cs286r/courses/fall09/papers/galeshapley.pdf ''Gale and Shapley (1962)'']]. */ class StableMarriageAlgorithm[M <: Proposer with Preferences[W], W <: Preferences[M]] - extends ((Set[M], Set[W]) => ((Set[M], Set[W]), Matching[W, M])) { + extends (((Set[M], Set[W])) => ((Set[M], Set[W]), Matching[W, M])) { /** Compute a stable matching between two sets of equal size. * - * @param ms set of proposers to be matched. - * @param ws set of proposees to be matched. + * @param unmatched * @return a stable matching between proposees (`ws`) and proposers (`ms`). */ - def apply(ms: Set[M], ws: Set[W]): ((Set[M], Set[W]), Matching[W, M]) = { - require(ms.size === ws.size) + def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), Matching[W, M]) = { + require(unmatched._1.size === unmatched._2.size) @annotation.tailrec def accumulate(unMatchedMs: Set[M], matches: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = { unMatchedMs.headOption match { case Some(unMatchedM) => val previouslyRejected = rejected.getOrElse(unMatchedM, Set.empty) - val mostPreferredW = ws.diff(previouslyRejected).max(unMatchedM.ordering) + val mostPreferredW = unmatched._2.diff(previouslyRejected).max(unMatchedM.ordering) matches.get(mostPreferredW) match { case Some(m) if mostPreferredW.ordering.lt(m, unMatchedM) => // mostPreferredW has received strictly better offer! val updatedUnMatchedMs = unMatchedMs - unMatchedM + m @@ -67,10 +66,10 @@ class StableMarriageAlgorithm[M <: Proposer with Preferences[W], W <: Preference } case None => assert(unMatchedMs.isEmpty) - (unMatchedMs, ws.diff(matches.keySet), matches) + (unMatchedMs, unmatched._2.diff(matches.keySet), matches) } } - val (unMatchedMs, unMatchedWs, matches) = accumulate(ms, Map.empty, Map.empty) + val (unMatchedMs, unMatchedWs, matches) = accumulate(unmatched._1, Map.empty, Map.empty) ((unMatchedMs, unMatchedWs), Matching(matches)) } diff --git a/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala b/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala index 589a1a7..71af6b0 100644 --- a/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala +++ b/src/performance/scala/org/economicsl/matching/DeferredAcceptanceMicroBenchmark.scala @@ -18,11 +18,11 @@ object DeferredAcceptanceMicroBenchmark extends Bench.OnlineRegressionReport { } def randomHouseListing(): HouseListing = { - HouseListing(UUID.randomUUID(), Random.nextLong(), Price(Random.nextLong()), randomHouse()) + HouseListing(UUID.randomUUID(), UUID.randomUUID(), Price(Random.nextLong()), randomHouse()) } def randomHousePurchaseOffer(): HousePurchaseOffer = { - HousePurchaseOffer(UUID.randomUUID(), Random.nextLong(), Price(Random.nextLong())) + HousePurchaseOffer(UUID.randomUUID(), UUID.randomUUID(), Price(Random.nextLong())) } def randomHousePurchaseOffers(size: Int): HashSet[HousePurchaseOffer] = { @@ -65,8 +65,8 @@ object DeferredAcceptanceMicroBenchmark extends Bench.OnlineRegressionReport { // } measure method "weaklyStableMatching" in { - using(unMatchedParticipants) in { case (buyers, sellers) => - (new StableMarriageAlgorithm[HousePurchaseOffer, HouseListing]())(buyers, sellers) + using(unMatchedParticipants) in { unmatched => + (new StableMarriageAlgorithm[HousePurchaseOffer, HouseListing]())(unmatched) } } diff --git a/src/test/scala/org/economicsl/matching/MarriageMarketSpecification.scala b/src/test/scala/org/economicsl/matching/MarriageMarketSpecification.scala index cd8c0bc..b5b9c08 100644 --- a/src/test/scala/org/economicsl/matching/MarriageMarketSpecification.scala +++ b/src/test/scala/org/economicsl/matching/MarriageMarketSpecification.scala @@ -15,6 +15,7 @@ limitations under the License. */ package org.economicsl.matching +import cats.data.State import org.scalacheck.{Gen, Properties} @@ -43,16 +44,16 @@ object MarriageMarketSpecification extends Properties("marriage-market") { } yield (ms, ws) } - property("all men and women are matched") = forAll(unMatched) { - case (ms, ws) => - val ((unMatchedMs, unMatchedWs), matching) = (new StableMarriageAlgorithm[Man, Woman])(ms, ws) - unMatchedMs.isEmpty && unMatchedWs.isEmpty && (matching.size == ms.size) + property("all men and women are matched") = forAll(unMatched) { unmatched => + val result = State(new StableMarriageAlgorithm[Man, Woman]).run(unmatched) + val ((unMatchedMs, unMatchedWs), matching) = result.value + unMatchedMs.isEmpty && unMatchedWs.isEmpty && (matching.size == unmatched._1.size) } - property("matching should be stable") = forAll(unMatched) { - case (ms, ws) => - val ((_, _), matching) = (new StableMarriageAlgorithm[Man, Woman])(ms, ws) - ms.forall(m => ws.forall(w => !matching.isBlockedBy(w -> m))) + property("matching should be stable") = forAll(unMatched) {unmatched => + val result = State(new StableMarriageAlgorithm[Man, Woman]).run(unmatched) + val ((_, _), matching) = result.value + unmatched._1.forall(m => unmatched._2.forall(w => !matching.isBlockedBy(w -> m))) } } diff --git a/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala b/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala index 7106665..bd0a36f 100644 --- a/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala +++ b/src/test/scala/org/economicsl/matching/WeaklyStableMatchingSpec.scala @@ -23,17 +23,17 @@ import org.scalatest.FlatSpec class WeaklyStableMatchingSpec extends FlatSpec { "A weakly stable matching of two empty sets" should "be an empty map" in { - val ((_, _), matching) = (new StableMarriageAlgorithm[Man, Woman])(Set.empty[Man], Set.empty[Woman]) + val ((_, _), matching) = (new StableMarriageAlgorithm[Man, Woman])((Set.empty[Man], Set.empty[Woman])) assert(matching.isEmpty) } it should "throw IllegalArgumentException when attempting to match sets of different size." in { assertThrows[IllegalArgumentException] { - (new StableMarriageAlgorithm[Man, Woman])(Set(Man(UUID.randomUUID(), 42, Man.womanByQuality)), Set.empty[Woman]) + (new StableMarriageAlgorithm[Man, Woman])((Set(Man(UUID.randomUUID(), 42, Man.womanByQuality)), Set.empty[Woman])) } assertThrows[IllegalArgumentException] { - (new StableMarriageAlgorithm[Man, Woman])(Set.empty[Man], Set(Woman(UUID.randomUUID(), 42, Woman.manByQuality))) + (new StableMarriageAlgorithm[Man, Woman])((Set.empty[Man], Set(Woman(UUID.randomUUID(), 42, Woman.manByQuality)))) } } From 10fa57238032a7ae707cb3c3017456bcd6b4f8ca Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Sun, 6 May 2018 09:58:08 +0300 Subject: [PATCH 3/7] Fixed missed merge conflict. --- .../matching/HouseMarketSpecification.scala | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala index c3e0502..f07ce4c 100644 --- a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala +++ b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala @@ -62,29 +62,16 @@ object HouseMarketSpecification extends Properties("housing-market") { } -<<<<<<< HEAD - property("matching should be incentive-compatible") = forAll(unMatched) { unmatched => + property("matching should be incentive-compatible") = Prop.forAll(unMatched) { unmatched => val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched) val ((_, _), matching) = result.value matching.matches.forall{ case (offer, listing) => offer.price >= listing.price } } - property("matching should be stable") = forAll(unMatched) { unmatched => + property("matching should be stable") = Prop.forAll(unMatched) { unmatched => val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched) val ((offers, listings), matching) = result.value offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o))) -======= - property("matching should be incentive-compatible") = Prop.forAll(housePurchaseOffers, houseListings) { - case (offers, listings) => - val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing])(offers, listings) - matching.matches.forall{ case (offer, listing) => offer.price >= listing.price } - } - - property("matching should be stable") = Prop.forAll(housePurchaseOffers, houseListings) { - case (offers, listings) => - val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing])(offers, listings) - offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o))) ->>>>>>> master } } From 857333d54bdc7fbfccb00e2840258b5343393f22 Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Sun, 6 May 2018 10:03:38 +0300 Subject: [PATCH 4/7] Added State monad to manytoone.DeferredAcceptanceAlgorith --- .../matching/HouseMarketSpecification.scala | 3 ++- .../manytoone/DeferredAcceptanceAlgorithm.scala | 9 +++++---- .../manytoone/SchoolChoiceSpecification.scala | 12 ++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala index f07ce4c..83c4b48 100644 --- a/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala +++ b/src/functional/scala/org/economicsl/matching/HouseMarketSpecification.scala @@ -70,7 +70,8 @@ object HouseMarketSpecification extends Properties("housing-market") { property("matching should be stable") = Prop.forAll(unMatched) { unmatched => val result = State(new DeferredAcceptanceAlgorithm[HousePurchaseOffer, HouseListing]).run(unmatched) - val ((offers, listings), matching) = result.value + val ((_, _), matching) = result.value + val (offers, listings) = unmatched offers.forall(o => listings.forall(l => !matching.isBlockedBy(l -> o))) } diff --git a/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala b/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala index f8d814d..75c7491 100644 --- a/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala +++ b/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala @@ -27,18 +27,18 @@ import scala.collection.immutable.TreeSet */ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferences[W], W <: Predicate[M] with Preferences[M] with Quota] - extends ((Set[M], Set[W]) => ((Set[M], Set[W]), ManyToOneMatching[W, M])) { + extends (((Set[M], Set[W])) => ((Set[M], Set[W]), ManyToOneMatching[W, M])) { /** Compute a stable matching between two sets. * - * @param ms set of proposers to be matched. - * @param ws set of proposees to be matched. + * @param unmatched * @return * @note A matching will be called "stable" unless there is a pair each of which strictly prefers the other * to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n` * is the size of the inputs sets. */ - def apply(ms: Set[M], ws: Set[W]): ((Set[M], Set[W]), ManyToOneMatching[W, M]) = { + def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), ManyToOneMatching[W, M]) = { + val (ms, ws) = unmatched @annotation.tailrec def accumulate(unMatched: Set[M], toBeMatched: Set[M], matches: Map[W, TreeSet[M]], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, TreeSet[M]]) = { @@ -85,6 +85,7 @@ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferenc (unMatched, matches.keySet.diff(ws), matches) } } + val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs) ((unMatchedMs, unMatchedWs), ManyToOneMatching(matches)) diff --git a/src/test/scala/org/economicsl/matching/manytoone/SchoolChoiceSpecification.scala b/src/test/scala/org/economicsl/matching/manytoone/SchoolChoiceSpecification.scala index 3f834a2..15d6fab 100644 --- a/src/test/scala/org/economicsl/matching/manytoone/SchoolChoiceSpecification.scala +++ b/src/test/scala/org/economicsl/matching/manytoone/SchoolChoiceSpecification.scala @@ -15,6 +15,7 @@ limitations under the License. */ package org.economicsl.matching.manytoone +import cats.data.State import org.scalacheck.{Gen, Prop, Properties} @@ -45,14 +46,17 @@ object SchoolChoiceSpecification extends Properties("school-choice") { } property("no school accepts more students than its quota allows") = Prop.forAll(unMatched) { - case (students, schools) => - val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[Student, School])(students, schools) + unmatched => + val result = State(new DeferredAcceptanceAlgorithm[Student, School]).run(unmatched) + val ((_, _), matching)= result.value matching.matches.forall{ case (school, matchedStudents) => matchedStudents.size <= school.quota } } property("matching should be stable") = Prop.forAll(unMatched) { - case (students, schools) => - val ((_, _), matching) = (new DeferredAcceptanceAlgorithm[Student, School])(students, schools) + unmatched => + val result = State(new DeferredAcceptanceAlgorithm[Student, School]).run(unmatched) + val ((_, _), matching)= result.value + val (students, schools) = unmatched students.forall(student => schools.forall(school => !matching.isBlockedBy(school -> student))) } From abc10a42e5dc1b7e1d16a04e1aba8ed7d64654e6 Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Sun, 6 May 2018 13:22:13 +0300 Subject: [PATCH 5/7] Added State monad back. --- .../onetoone/StableMarriageAlgorithm.scala | 13 ++++++----- .../MarriageMarketSpecification.scala | 22 ++++++++++--------- .../StableMarriageAlgorithmSpec.scala | 13 ++++++----- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/main/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithm.scala b/src/main/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithm.scala index d4f5569..8bcb380 100644 --- a/src/main/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithm.scala +++ b/src/main/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithm.scala @@ -38,21 +38,22 @@ import scala.util.{Failure, Success, Try} * [[http://www.eecs.harvard.edu/cs286r/courses/fall09/papers/galeshapley.pdf ''Gale and Shapley (1962)'']]. */ class StableMarriageAlgorithm[M <: Proposer with Preferences[W], W <: Preferences[M]] - extends ((Set[M], Set[W]) => Try[((Set[M], Set[W]), OneToOneMatching[W, M])]) { + extends (((Set[M], Set[W])) => ((Set[M], Set[W]), Try[OneToOneMatching[W, M]])) { /** Compute a stable matching between two sets of equal size. * * @param unmatched * @return a stable matching between proposees (`ws`) and proposers (`ms`). */ - def apply(ms: Set[M], ws: Set[W]): Try[((Set[M], Set[W]), OneToOneMatching[W, M])] = { + def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), Try[OneToOneMatching[W, M]]) = { + val (ms, ws) = unmatched @annotation.tailrec def accumulate(unMatchedMs: Set[M], matches: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = { unMatchedMs.headOption match { case Some(unMatchedM) => val previouslyRejected = rejected.getOrElse(unMatchedM, Set.empty) - val mostPreferredW = unmatched._2.diff(previouslyRejected).max(unMatchedM.ordering) + val mostPreferredW = ws.diff(previouslyRejected).max(unMatchedM.ordering) matches.get(mostPreferredW) match { case Some(m) if mostPreferredW.ordering.lt(m, unMatchedM) => // mostPreferredW has received strictly better offer! val updatedUnMatchedMs = unMatchedMs - unMatchedM + m @@ -67,16 +68,16 @@ class StableMarriageAlgorithm[M <: Proposer with Preferences[W], W <: Preference } case None => assert(unMatchedMs.isEmpty) - (unMatchedMs, unmatched._2.diff(matches.keySet), matches) + (unMatchedMs, ws.diff(matches.keySet), matches) } } if (ms.size == ws.size) { val (unMatchedMs, unMatchedWs, matches) = accumulate(ms, Map.empty, Map.empty) - Success(((unMatchedMs, unMatchedWs), OneToOneMatching(matches))) + ((unMatchedMs, unMatchedWs), Success(OneToOneMatching(matches))) } else { - Failure(new IllegalArgumentException(s"The size of ms is ${ms.size} which does not equal the size of ws which is ${ws.size}!")) + ((ms, ws), Failure(new IllegalArgumentException(s"The size of ms is ${ms.size} which does not equal the size of ws which is ${ws.size}!"))) } } diff --git a/src/test/scala/org/economicsl/matching/onetoone/MarriageMarketSpecification.scala b/src/test/scala/org/economicsl/matching/onetoone/MarriageMarketSpecification.scala index 73034fa..2da934a 100644 --- a/src/test/scala/org/economicsl/matching/onetoone/MarriageMarketSpecification.scala +++ b/src/test/scala/org/economicsl/matching/onetoone/MarriageMarketSpecification.scala @@ -48,22 +48,24 @@ object MarriageMarketSpecification extends Properties("marriage-market") { } property("all men and women are matched") = Prop.forAll(randomUnmatchedSets) { - case (ms, ws) => - val result = State(new StableMarriageAlgorithm[Man, Woman]).run(ms, ws) - result match { - case Success(((unMatchedMs, unMatchedWs), matching)) => - (ms.size == ws.size) && unMatchedMs.isEmpty && unMatchedWs.isEmpty && (matching.size == ms.size) + case unmatched @ (ms, ws) => + val result = State(new StableMarriageAlgorithm[Man, Woman]).run(unmatched) + val ((unMatchedMs, unMatchedWs), matching) = result.value + matching match { + case Success(oneToOneMatching) => + (ms.size == ws.size) && unMatchedMs.isEmpty && unMatchedWs.isEmpty && (oneToOneMatching.size == ms.size) case Failure(_) => ms.size != ws.size } } property("matching should be stable") = Prop.forAll(randomUnmatchedSets) { - case (ms, ws) => - val result = State(new StableMarriageAlgorithm[Man, Woman]).run(ms, ws) - result match { - case Success(((_, _), matching)) => - ms.forall(m => ws.forall(w => !matching.isBlockedBy(w -> m))) + case unmatched @ (ms, ws) => + val result = State(new StableMarriageAlgorithm[Man, Woman]).run(unmatched) + val ((_, _), matching) = result.value + matching match { + case Success(oneToOneMatching) => + ms.forall(m => ws.forall(w => !oneToOneMatching.isBlockedBy(w -> m))) case Failure(_) => ms.size != ws.size } diff --git a/src/test/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithmSpec.scala b/src/test/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithmSpec.scala index 7c88de6..e28ba59 100644 --- a/src/test/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithmSpec.scala +++ b/src/test/scala/org/economicsl/matching/onetoone/StableMarriageAlgorithmSpec.scala @@ -25,20 +25,23 @@ import scala.util.{Failure, Success} class StableMarriageAlgorithmSpec extends FlatSpec { "The stable marriage algorithm applied to two empty sets" should "successfully return an empty matching." in { - val result = (new StableMarriageAlgorithm[Man, Woman])(Set.empty[Man], Set.empty[Woman]) + val unmatched = (Set.empty[Man], Set.empty[Woman]) + val result = (new StableMarriageAlgorithm[Man, Woman])(unmatched) result match { - case Success(((_, _), matching)) => + case ((_, _), Success(matching)) => assert(matching.isEmpty) - case Failure(_) => + case ((_,_), Failure(_)) => false } } "The stable matching algorithm applied to sets of different sizes" should "return a failure." in { - val result1 = (new StableMarriageAlgorithm[Man, Woman])(Set(Man(UUID.randomUUID(), 42, Man.womanByQuality)), Set.empty[Woman]) + val unmatched1 = (Set(Man(UUID.randomUUID(), 42, Man.womanByQuality)), Set.empty[Woman]) + val ((_, _), result1) = (new StableMarriageAlgorithm[Man, Woman])(unmatched1) assert(result1.isFailure) - val result2 = (new StableMarriageAlgorithm[Man, Woman])(Set.empty[Man], Set(Woman(UUID.randomUUID(), 42, Woman.manByQuality))) + val unmatched2 = (Set.empty[Man], Set(Woman(UUID.randomUUID(), 42, Woman.manByQuality))) + val ((_, _), result2) = (new StableMarriageAlgorithm[Man, Woman])(unmatched2) assert(result2.isFailure) } From ca859c7ef7cfe234505f39fe6c76aaee10cf5e52 Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Sun, 6 May 2018 13:33:01 +0300 Subject: [PATCH 6/7] Fixed unnamed params. --- .../matching/onetoone/DeferredAcceptanceAlgorithm.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/scala/org/economicsl/matching/onetoone/DeferredAcceptanceAlgorithm.scala b/src/main/scala/org/economicsl/matching/onetoone/DeferredAcceptanceAlgorithm.scala index 478c973..9cbfb9e 100644 --- a/src/main/scala/org/economicsl/matching/onetoone/DeferredAcceptanceAlgorithm.scala +++ b/src/main/scala/org/economicsl/matching/onetoone/DeferredAcceptanceAlgorithm.scala @@ -35,13 +35,14 @@ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferenc * is the size of the inputs sets. */ def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), OneToOneMatching[W, M]) = { + val (ms, ws) = unmatched @annotation.tailrec def accumulate(unMatchedMs: Set[M], toBeMatchedMs: Set[M], matches: Map[W, M], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, M]) = { toBeMatchedMs.headOption match { case Some(toBeMatchedM) => val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty) - val acceptableWs = unmatched._2.diff(previouslyRejected) + val acceptableWs = ws.diff(previouslyRejected) if (acceptableWs.isEmpty) { accumulate(unMatchedMs + toBeMatchedM, toBeMatchedMs - toBeMatchedM, matches, rejected) } else { @@ -64,11 +65,11 @@ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferenc } } case None => - (unMatchedMs, matches.keySet.diff(unmatched._2), matches) + (unMatchedMs, matches.keySet.diff(ws), matches) } } - val unacceptableWs = unmatched._1.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> unmatched._2.filter(m.isAcceptable))) - val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, unmatched._1, Map.empty, unacceptableWs) + val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) + val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs) ((unMatchedMs, unMatchedWs), OneToOneMatching(matches)) } From 9af8b87e91d7069a5a6b2230f2dd0ed67ff42044 Mon Sep 17 00:00:00 2001 From: davidrpugh Date: Sun, 6 May 2018 13:43:18 +0300 Subject: [PATCH 7/7] fixed code alignment. --- .../DeferredAcceptanceAlgorithm.scala | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala b/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala index 75c7491..f238197 100644 --- a/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala +++ b/src/main/scala/org/economicsl/matching/manytoone/DeferredAcceptanceAlgorithm.scala @@ -27,68 +27,68 @@ import scala.collection.immutable.TreeSet */ class DeferredAcceptanceAlgorithm[M <: Proposer with Predicate[W] with Preferences[W], W <: Predicate[M] with Preferences[M] with Quota] - extends (((Set[M], Set[W])) => ((Set[M], Set[W]), ManyToOneMatching[W, M])) { + extends (((Set[M], Set[W])) => ((Set[M], Set[W]), ManyToOneMatching[W, M])) { - /** Compute a stable matching between two sets. - * - * @param unmatched - * @return - * @note A matching will be called "stable" unless there is a pair each of which strictly prefers the other - * to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n` - * is the size of the inputs sets. - */ - def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), ManyToOneMatching[W, M]) = { - val (ms, ws) = unmatched + /** Compute a stable matching between two sets. + * + * @param unmatched + * @return + * @note A matching will be called "stable" unless there is a pair each of which strictly prefers the other + * to its partner in the matching. This algorithm produces a weakly stable matching in `O(n^2)` time where `n` + * is the size of the inputs sets. + */ + def apply(unmatched: (Set[M], Set[W])): ((Set[M], Set[W]), ManyToOneMatching[W, M]) = { + val (ms, ws) = unmatched - @annotation.tailrec - def accumulate(unMatched: Set[M], toBeMatched: Set[M], matches: Map[W, TreeSet[M]], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, TreeSet[M]]) = { - toBeMatched.headOption match { - case Some(toBeMatchedM) => - val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty) - val acceptableWs = ws.diff(previouslyRejected) - if (acceptableWs.isEmpty) { - accumulate(unMatched + toBeMatchedM, toBeMatched - toBeMatchedM, matches, rejected) - } else { - val mostPreferredW = acceptableWs.max(toBeMatchedM.ordering) - matches.get(mostPreferredW) match { - case Some(matchedMs) if mostPreferredW.isAcceptable(toBeMatchedM) => - matchedMs.headOption match { - case Some(_) if matchedMs.size < mostPreferredW.quota => - val updatedToBeMatchedMs = toBeMatched - toBeMatchedM - val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM) - accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected) - case Some(leastPreferredMatchedM) if mostPreferredW.ordering.lt(leastPreferredMatchedM, toBeMatchedM) => - val updatedToBeMatchedMs = toBeMatched - toBeMatchedM + leastPreferredMatchedM - val updatedMatches = matches.updated(mostPreferredW, matchedMs - leastPreferredMatchedM + toBeMatchedM) - val updatedRejected = rejected.updated(leastPreferredMatchedM, rejected.getOrElse(leastPreferredMatchedM, Set.empty) + mostPreferredW) - accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, updatedRejected) - case Some(leastPreferredMatchedM) if mostPreferredW.ordering.gteq(leastPreferredMatchedM, toBeMatchedM) => - val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) - accumulate(unMatched, toBeMatched, matches, updatedRejected) - case None => - val updatedToBeMatchedMs = toBeMatched - toBeMatchedM - val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM) - accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected) - } - case Some(_) if mostPreferredW.isNotAcceptable(toBeMatchedM) => - val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) - accumulate(unMatched, toBeMatched, matches, updatedRejected) - case None if mostPreferredW.isAcceptable(toBeMatchedM) => - val updatedMatches = matches + (mostPreferredW -> TreeSet(toBeMatchedM)(mostPreferredW.ordering)) - accumulate(unMatched, toBeMatched - toBeMatchedM, updatedMatches, rejected) - case None if mostPreferredW.isNotAcceptable(toBeMatchedM) => - val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) - accumulate(unMatched, toBeMatched, matches, updatedRejected) - } + @annotation.tailrec + def accumulate(unMatched: Set[M], toBeMatched: Set[M], matches: Map[W, TreeSet[M]], rejected: Map[M, Set[W]]): (Set[M], Set[W], Map[W, TreeSet[M]]) = { + toBeMatched.headOption match { + case Some(toBeMatchedM) => + val previouslyRejected = rejected.getOrElse(toBeMatchedM, Set.empty) + val acceptableWs = ws.diff(previouslyRejected) + if (acceptableWs.isEmpty) { + accumulate(unMatched + toBeMatchedM, toBeMatched - toBeMatchedM, matches, rejected) + } else { + val mostPreferredW = acceptableWs.max(toBeMatchedM.ordering) + matches.get(mostPreferredW) match { + case Some(matchedMs) if mostPreferredW.isAcceptable(toBeMatchedM) => + matchedMs.headOption match { + case Some(_) if matchedMs.size < mostPreferredW.quota => + val updatedToBeMatchedMs = toBeMatched - toBeMatchedM + val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM) + accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected) + case Some(leastPreferredMatchedM) if mostPreferredW.ordering.lt(leastPreferredMatchedM, toBeMatchedM) => + val updatedToBeMatchedMs = toBeMatched - toBeMatchedM + leastPreferredMatchedM + val updatedMatches = matches.updated(mostPreferredW, matchedMs - leastPreferredMatchedM + toBeMatchedM) + val updatedRejected = rejected.updated(leastPreferredMatchedM, rejected.getOrElse(leastPreferredMatchedM, Set.empty) + mostPreferredW) + accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, updatedRejected) + case Some(leastPreferredMatchedM) if mostPreferredW.ordering.gteq(leastPreferredMatchedM, toBeMatchedM) => + val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) + accumulate(unMatched, toBeMatched, matches, updatedRejected) + case None => + val updatedToBeMatchedMs = toBeMatched - toBeMatchedM + val updatedMatches = matches.updated(mostPreferredW, matchedMs + toBeMatchedM) + accumulate(unMatched, updatedToBeMatchedMs, updatedMatches, rejected) + } + case Some(_) if mostPreferredW.isNotAcceptable(toBeMatchedM) => + val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) + accumulate(unMatched, toBeMatched, matches, updatedRejected) + case None if mostPreferredW.isAcceptable(toBeMatchedM) => + val updatedMatches = matches + (mostPreferredW -> TreeSet(toBeMatchedM)(mostPreferredW.ordering)) + accumulate(unMatched, toBeMatched - toBeMatchedM, updatedMatches, rejected) + case None if mostPreferredW.isNotAcceptable(toBeMatchedM) => + val updatedRejected = rejected.updated(toBeMatchedM, previouslyRejected + mostPreferredW) + accumulate(unMatched, toBeMatched, matches, updatedRejected) } - case None => - (unMatched, matches.keySet.diff(ws), matches) - } + } + case None => + (unMatched, matches.keySet.diff(ws), matches) } - - val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) - val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs) - ((unMatchedMs, unMatchedWs), ManyToOneMatching(matches)) } + val unacceptableWs = ms.foldLeft(Map.empty[M, Set[W]])((z, m) => z + (m -> ws.filter(m.isAcceptable))) + val (unMatchedMs, unMatchedWs, matches) = accumulate(Set.empty, ms, Map.empty, unacceptableWs) + ((unMatchedMs, unMatchedWs), ManyToOneMatching(matches)) } + +}