0
# Scalactic
1
2
Scalactic is a Scala library that provides functional programming utilities for equality, constraints, and error handling. It offers an alternative to exceptions with the Or type, value classes for ensuring constraints, and utilities for customizing equality comparisons.
3
4
## Capabilities
5
6
### Or Type (Union Types)
7
8
The Or type represents a value that can be one of two types - either a "Good" success value or a "Bad" error value.
9
10
```scala { .api }
11
/**
12
* Union type representing either a good value or a bad value
13
*/
14
sealed abstract class Or[+G, +B] extends Product with Serializable {
15
def isGood: Boolean
16
def isBad: Boolean
17
def get: G
18
def getBadOrElse[BB >: B](default: => BB): BB
19
20
/**
21
* Transform the good value while preserving bad values
22
*/
23
def map[H](f: G => H): H Or B
24
25
/**
26
* FlatMap operation for chaining Or operations
27
*/
28
def flatMap[H, C >: B](f: G => H Or C): H Or C
29
30
/**
31
* Apply a function to transform bad values
32
*/
33
def badMap[C](f: B => C): G Or C
34
35
/**
36
* Fold the Or value by applying functions to both cases
37
*/
38
def fold[T](fa: B => T, fb: G => T): T
39
}
40
41
/**
42
* Represents a successful result
43
*/
44
final case class Good[+G](get: G) extends Or[G, Nothing] {
45
def isGood: Boolean = true
46
def isBad: Boolean = false
47
}
48
49
/**
50
* Represents an error result
51
*/
52
final case class Bad[+B](get: B) extends Or[Nothing, B] {
53
def isGood: Boolean = false
54
def isBad: Boolean = true
55
}
56
```
57
58
**Usage Examples:**
59
60
```scala
61
import org.scalactic._
62
63
// Creating Or values
64
val goodResult: Int Or String = Good(42)
65
val badResult: Int Or String = Bad("Error occurred")
66
67
// Safe operations that return Or instead of throwing exceptions
68
def divide(x: Int, y: Int): Int Or String = {
69
if (y == 0) Bad("Division by zero")
70
else Good(x / y)
71
}
72
73
// Chaining operations with map
74
val result = Good(10) map (_ * 2) map (_ + 1)
75
// Result: Good(21)
76
77
// FlatMap for chaining operations that return Or
78
val chainedResult = for {
79
a <- divide(10, 2) // Good(5)
80
b <- divide(a, 3) // Bad("Division by zero") if a was 0
81
c <- divide(b, 1) // Only executes if previous succeeded
82
} yield c
83
84
// Pattern matching
85
goodResult match {
86
case Good(value) => println(s"Success: $value")
87
case Bad(error) => println(s"Error: $error")
88
}
89
```
90
91
### Attempt Function
92
93
Safely execute code that might throw exceptions, wrapping results in Or.
94
95
```scala { .api }
96
/**
97
* Execute code safely, wrapping exceptions in Bad and results in Good
98
*/
99
def attempt[R](f: => R): R Or Throwable
100
```
101
102
**Usage Examples:**
103
104
```scala
105
import org.scalactic._
106
107
// Safe string to integer conversion
108
val parseResult = attempt { "42".toInt }
109
// Result: Good(42)
110
111
val parseError = attempt { "not-a-number".toInt }
112
// Result: Bad(NumberFormatException)
113
114
// Chaining with other operations
115
val computation = for {
116
num <- attempt { "42".toInt }
117
doubled <- Good(num * 2)
118
result <- attempt { s"Result: $doubled" }
119
} yield result
120
```
121
122
### Value Classes (AnyVals)
123
124
Type-safe wrappers that ensure values meet certain constraints without runtime overhead.
125
126
```scala { .api }
127
/**
128
* A string that cannot be empty
129
*/
130
final class NonEmptyString private (val value: String) extends AnyVal {
131
override def toString: String = value
132
}
133
134
object NonEmptyString {
135
/**
136
* Create a NonEmptyString from a regular string
137
*/
138
def from(value: String): NonEmptyString Or One[ErrorMessage]
139
def apply(value: String): NonEmptyString // Throws if empty
140
def unapply(nonEmptyString: NonEmptyString): Some[String]
141
}
142
143
/**
144
* A list that cannot be empty
145
*/
146
final class NonEmptyList[+T] private (val toList: List[T]) extends AnyVal {
147
def head: T
148
def tail: List[T]
149
def length: Int
150
def map[U](f: T => U): NonEmptyList[U]
151
def flatMap[U](f: T => NonEmptyList[U]): NonEmptyList[U]
152
}
153
154
object NonEmptyList {
155
def apply[T](firstElement: T, otherElements: T*): NonEmptyList[T]
156
def from[T](list: List[T]): NonEmptyList[T] Or One[ErrorMessage]
157
}
158
159
/**
160
* Similar value classes for other collections
161
*/
162
final class NonEmptyVector[+T] private (val toVector: Vector[T]) extends AnyVal
163
final class NonEmptyArray[T] private (val toArray: Array[T]) extends AnyVal
164
final class NonEmptySet[T] private (val toSet: Set[T]) extends AnyVal
165
final class NonEmptyMap[K, +V] private (val toMap: Map[K, V]) extends AnyVal
166
```
167
168
**Usage Examples:**
169
170
```scala
171
import org.scalactic.anyvals._
172
173
// Safe construction with error handling
174
val nameResult = NonEmptyString.from("John")
175
// Result: Good(NonEmptyString("John"))
176
177
val emptyResult = NonEmptyString.from("")
178
// Result: Bad(One(""))
179
180
// Direct construction (throws if constraint violated)
181
val name = NonEmptyString("John Doe")
182
println(name.value) // "John Doe"
183
184
// Working with NonEmptyList
185
val numbers = NonEmptyList(1, 2, 3, 4, 5)
186
val doubled = numbers.map(_ * 2)
187
val summed = numbers.flatMap(n => NonEmptyList(n, n * 10))
188
189
// Pattern matching
190
name match {
191
case NonEmptyString(value) => println(s"Name: $value")
192
}
193
```
194
195
### Equality and Constraints
196
197
Customizable equality comparisons and type constraints.
198
199
```scala { .api }
200
/**
201
* Trait for defining custom equality
202
*/
203
trait Equality[A] {
204
def areEqual(a: A, b: Any): Boolean
205
}
206
207
/**
208
* Trait for defining equivalence relations
209
*/
210
trait Equivalence[T] {
211
def equiv(a: T, b: T): Boolean
212
}
213
214
/**
215
* Default equality implementations
216
*/
217
object Equality {
218
/**
219
* Default equality based on universal equality
220
*/
221
implicit def default[A]: Equality[A]
222
223
/**
224
* Tolerance-based equality for floating point numbers
225
*/
226
def tolerantDoubleEquality(tolerance: Double): Equality[Double]
227
def tolerantFloatEquality(tolerance: Float): Equality[Float]
228
}
229
230
/**
231
* Triple equals with constraint checking
232
*/
233
trait TripleEquals {
234
def ===[T](right: T)(implicit constraint: CanEqual[T, T]): Boolean
235
def !==[T](right: T)(implicit constraint: CanEqual[T, T]): Boolean
236
}
237
238
/**
239
* Type constraint for equality comparisons
240
*/
241
trait CanEqual[-A, -B] {
242
def areEqual(a: A, b: B): Boolean
243
}
244
```
245
246
**Usage Examples:**
247
248
```scala
249
import org.scalactic._
250
251
// Custom equality for case-insensitive strings
252
implicit val stringEquality = new Equality[String] {
253
def areEqual(a: String, b: Any): Boolean = b match {
254
case s: String => a.toLowerCase == s.toLowerCase
255
case _ => false
256
}
257
}
258
259
// Triple equals with constraints
260
import TripleEquals._
261
"Hello" === "HELLO" // true with custom equality
262
263
// Tolerance-based floating point comparison
264
implicit val doubleEquality = Equality.tolerantDoubleEquality(0.01)
265
3.14159 === 3.14200 // true within tolerance
266
```
267
268
### Normalization
269
270
Transform values before comparison or processing.
271
272
```scala { .api }
273
/**
274
* Trait for normalizing values before operations
275
*/
276
trait Normalization[A] {
277
def normalized(a: A): A
278
}
279
280
/**
281
* Uniformity combines normalization with equality
282
*/
283
trait Uniformity[A] extends Normalization[A] with Equality[A] {
284
final def areEqual(a: A, b: Any): Boolean = b match {
285
case bAsA: A => normalized(a) == normalized(bAsA)
286
case _ => false
287
}
288
}
289
290
/**
291
* Pre-built string normalizations
292
*/
293
object StringNormalizations {
294
/**
295
* Normalize by trimming whitespace
296
*/
297
val trimmed: Normalization[String]
298
299
/**
300
* Normalize to lowercase
301
*/
302
val lowerCased: Normalization[String]
303
304
/**
305
* Remove all whitespace
306
*/
307
val removeAllWhitespace: Normalization[String]
308
}
309
```
310
311
**Usage Examples:**
312
313
```scala
314
import org.scalactic._
315
import StringNormalizations._
316
317
// Custom normalization
318
implicit val trimmedStringEquality = new Uniformity[String] {
319
def normalized(s: String): String = s.trim
320
}
321
322
// Using pre-built normalizations
323
val normalizer = lowerCased and trimmed
324
val result = normalizer.normalized(" HELLO WORLD ")
325
// Result: "hello world"
326
```
327
328
### Requirements and Validation
329
330
Assertion-like functionality that returns results instead of throwing exceptions.
331
332
```scala { .api }
333
trait Requirements {
334
/**
335
* Require a condition, returning Good(Unit) or Bad with message
336
*/
337
def require(condition: Boolean): Unit Or ErrorMessage
338
def require(condition: Boolean, message: => Any): Unit Or ErrorMessage
339
340
/**
341
* Require non-null value
342
*/
343
def requireNonNull[T](obj: T): T Or ErrorMessage
344
def requireNonNull[T](obj: T, message: => Any): T Or ErrorMessage
345
}
346
347
object Requirements extends Requirements
348
```
349
350
**Usage Examples:**
351
352
```scala
353
import org.scalactic.Requirements._
354
355
// Validation with requirements
356
def createUser(name: String, age: Int): User Or ErrorMessage = {
357
for {
358
_ <- require(name.nonEmpty, "Name cannot be empty")
359
_ <- require(age >= 0, "Age cannot be negative")
360
_ <- require(age <= 150, "Age must be realistic")
361
} yield User(name, age)
362
}
363
364
val validUser = createUser("John", 25) // Good(User("John", 25))
365
val invalidUser = createUser("", -5) // Bad("Name cannot be empty")
366
```
367
368
## Types
369
370
```scala { .api }
371
/**
372
* Type alias for error messages
373
*/
374
type ErrorMessage = String
375
376
/**
377
* Container for a single error message
378
*/
379
final case class One[+T](value: T) extends AnyVal
380
381
/**
382
* Container for multiple error messages
383
*/
384
final case class Many[+T](values: Iterable[T]) extends AnyVal
385
386
/**
387
* Union of One and Many for error accumulation
388
*/
389
type Every[+T] = One[T] Or Many[T]
390
```