Notes
- Environments: Python 3.10, Scala 2.13.2
- To enhance the readability of the algorithm implementations, we have omitted non-essential code elements like error checking, comments, exceptions, validation of class and method arguments, scoping qualifiers, and import statements.
Overview
- Write a sequence of if/elif/else condition - action pairs.
- Create a dictionary with key as condition and value as action.
Pattern matching in Scala
Typed pattern matching has been part of the Scala programming language for the last 10 years [ref 3]. The purpose is to check a value against one or several values or patterns. This feature is more powerful than the switch statement found in most of programming language as it can deconstructs a value or class instance into its constituent parts.
In the following example the type of a status instance (inherited from trait Status) is matched against all possible types (Failure, Success and Unknown).
sealed trait Status
case class Failure(error: String) extends Status
case object Success extends Status
case object Unknown extends Status
def processStatus(status: Status): String = status match {
case Failure(errorMsg) => errorMsg
case Success => "Succeeded"
case Unknown => "Undefined status"
}
Note that the set of types derived from Status is sealed (or restricted). Therefore the function processStatus does not need to handle undefined types (already checked by the compiler).
Python value-type pattern
from typing import Any
from enum import Enum
class EnumValue(Enum):
SUCCESS = 1
FAILURE = -1
UNKNOWN = 0
def variable_matching(value: Any):
match value:
case 2.0:
print(f'Input {value} is match as a float')
case "3.5":
print(f'Input {value} is match as a string')
case EnumValue.SUCCESS:
print(f'Success')
case _:
print(f'Failed to match {value}')
if __name__ == '__main__':
variable_matching(3.5) # Failed to match 3.5
variable_matching("3.5") # 3.5 is matched as a string
variable_matching(EnumValue.FAILURE) # Failed to match EnumValue.FAILURE
Matched type | Matched value | Outcome |
No | No | Failed |
Yes | No | Failed |
No | Yes | Failed |
Yes | Yes | Succeed |
Python mappings pattern
from typing import Dict, Optional
# Dict keys: 'name', 'status', 'role', 'bonus'
def mappings_matching(json_dict: Dict) -> Optional[Dict]:
match json_dict:
case {'name': 'Joan'}:
json_dict['status'] = 'vacation'
return json_dict
case {'role': 'engineer', 'status': 'promoted'}:
json_dict['bonus'] = True
return json_dict
case _:
print(f'ERROR: {str(json_dict)} not supported')
return None
if __name__ == '__main__': json_object = { 'name': 'Joan', 'status': 'full-time', 'role': 'marketing director', 'bonus': False } print(mappings_matching(json_object)) # {'name': 'Joan', 'status': 'vacation', 'role': 'marketing director', 'bonus': False} json_object = { 'name': 'Frank', 'status': 'promoted', 'role': 'engineer', 'bonus': False } print(mappings_matching(json_object)) # {'name': 'Frank', 'status': 'promoted', 'role': 'engineer', 'bonus': True} json_object = { 'name': 'Frank', 'status': 'promoted', 'role': 'account manager', 'bonus': False } print(mappings_matching(json_object)) # ERROR: {'name': 'Frank', 'status': 'promoted', 'role': 'account manager', 'bonus': False} not supported
Python class pattern
- name of the operator with type string
- arguments, args, of the operator with a type tuple.
- supported (name as key)
- has exactly two arguments
from typing import Any, AnyStr, Tuple
from dataclasses import dataclass
@dataclass
class Operator:
name: AnyStr
args: Tuple
def __call__(self) -> AnyStr:
match (self.name, len(self.args)): # Minimum condition for matching
case ('multiply', 2):
value = self.args[0]*self.args[1]
return f'{self.args[0]} x {self.args[1]} = {value}'
case ('add', 2):
value = self.args[0] + self.args[1]
return f'{self.args[0]} + {self.args[1]} = {value}'
case _:
return "Undefined operation"
def __str__(self):
return f'{self.name}: {str(self.args)}'
if __name__ == '__main__':
operator = Operator("add", (3.5, 6.2))
print(operator) # add: (3.5, 6.2)
- Match the type of input to Operator
- Match the attributes of the operator by invoking the method Operator.__call__ described above.
def object_matching(obj: Any) -> AnyStr:
match obj: # First match: Is an operator?
case Operator('multiply', _): # Second match: Are operator attributes valid?
return obj()
case Operator(_, _):
return obj()
case _:
return f'Type not find {str(obj)}'
if __name__ == '__main__':
operator = Operator("add", (3.5, 6.2))
print(object_matching(operator)) # 3.5 + 6.2 = 9.7
operator = Operator("multiply", (3, 2))
print(object_matching(operator)) # 3 x 2 = 6
operator = Operator("multiply", (1, 3, 9))operator = Operator("divided", (3, 3)) print(object_matching(operator)) vvvv# Undefined operation print(object_matching(3.4)) b. # Type not find 3.4
print(object_matching(operator)) # Undefined operation
This post illustrates some of the applications of the structural pattern matching feature introduced in version 3.10. There are many more patterns that worth exploring [4].
References
He has been director of data engineering at Aideo Technologies since 2017 and he is the author of "Scala for Machine Learning" Packt Publishing ISBN 978-1-78712-238-3