Solution (source code)

= Solution

For the <product of finite probability distributions>, enumerate the <Cartesian product> of the two supports and multiply the two <probability masses>.
``
def product[T,U](d: FiniteDist[T], d2: FiniteDist[U]): FiniteDist[(T,U)] =
  new FiniteDist[(T,U)] {
    private val dom = for (x <- d.getDom; y <- d2.getDom) yield (x,y)
    def getDom: List[(T,U)] = dom
    def getProb(p: (T,U)): Double = d.getProb(p._1) * d2.getProb(p._2)
  }
``
Each pair appears once because both original supports have no duplicates. Exact <probabilities> are positive on that support, and normalization follows by finite distributivity:
$$
\sum_{x,y}p_1(x)p_2(y)=\left(\sum_xp_1(x)\right)\left(\sum_yp_2(y)\right)=1.
$$
For the <pushforward of a finite probability distribution>, sum all masses in each <fiber of a function>; simply overwriting repeated image values would lose <probability>.
``
def image[T,U](d: FiniteDist[T], f: T => U): FiniteDist[U] =
  new FiniteDist[U] {
    private val weighted = d.getDom.map(x => (f(x), d.getProb(x)))
    private val dom = weighted.map(_._1).distinct
    def getDom: List[U] = dom
    def getProb(y: U): Double =
      weighted.foldLeft(0.0) { case (total, (z,p)) =>
        if (z == y) total+p else total
      }
  }
``
Assume the given <finite probability distributions> are fixed and `f` is a pure total <function> on the source support. Distinct image values form exactly the positive-mass support. The <fibers> partition the original support, proving
$$
\boxed{p_{\mathrm{product}}(x,y)=p_1(x)p_2(y),\qquad p_{\mathrm{image}}(z)=\sum_{x:f(x)=z}p_1(x),\qquad\sum_zp_{\mathrm{image}}(z)=1.}
$$
For example, sending a <discrete uniform distribution> on $\{1,2,3,4\}$ to parity produces masses $1/2$ and $1/2$, not four distinct outcomes. Exact algebra establishes the <finite probability distribution> laws; `Double` computes approximations, with the usual possibility of rounding and <floating-point underflow> for very small products.