0
# Scalactic Utilities
1
2
Scalactic provides functional programming utilities and better error handling mechanisms for Scala applications. The library focuses on functional idioms, type-safe error handling with the Or type, and pluggable equality systems.
3
4
## Capabilities
5
6
### Or Type System
7
8
Railway-oriented programming with Good/Bad disjunction type for error handling.
9
10
```scala { .api }
11
import org.scalactic._
12
13
// Core Or type
14
sealed abstract class Or[+G, +B] {
15
// Type checking methods
16
def isGood: Boolean
17
def isBad: Boolean
18
19
// Value extraction (unsafe)
20
def get: G // Throws if Bad
21
22
// Safe value extraction
23
def getOrElse[H >: G](alternative: => H): H
24
25
// Transformation methods
26
def map[H](f: G => H): H Or B
27
def flatMap[H, C >: B](f: G => H Or C): H Or C
28
def filter[C >: B](p: G => Boolean): G Or (B Or C)
29
def foreach(f: G => Unit): Unit
30
def fold[V](fa: B => V, fb: G => V): V
31
32
// Collection-like operations
33
def toOption: Option[G]
34
def toSeq: Seq[G]
35
def iterator: Iterator[G]
36
37
// Combination methods
38
def orElse[H >: G, C >: B](alternative: => H Or C): H Or C
39
def recover[H >: G](pf: PartialFunction[B, H]): H Or B
40
def recoverWith[H >: G, C >: B](pf: PartialFunction[B, H Or C]): H Or C
41
42
// Validation methods
43
def badMap[C](f: B => C): G Or C
44
def swap: B Or G
45
def exists(p: G => Boolean): Boolean
46
def forall(p: G => Boolean): Boolean
47
}
48
49
// Concrete implementations
50
case class Good[+G](value: G) extends Or[G, Nothing]
51
case class Bad[+B](value: B) extends Or[Nothing, B]
52
53
// Companion object methods
54
object Or {
55
def apply[G, B](value: G): G Or B = Good(value)
56
def good[G, B](value: G): G Or B = Good(value)
57
def bad[G, B](value: B): G Or B = Bad(value)
58
}
59
```
60
61
**Or Type Examples:**
62
```scala
63
import org.scalactic._
64
65
// Basic usage
66
def divide(x: Int, y: Int): Int Or String = {
67
if (y == 0) Bad("Cannot divide by zero")
68
else Good(x / y)
69
}
70
71
val result1 = divide(10, 2) // Good(5)
72
val result2 = divide(10, 0) // Bad("Cannot divide by zero")
73
74
// Pattern matching
75
result1 match {
76
case Good(value) => println(s"Result: $value")
77
case Bad(error) => println(s"Error: $error")
78
}
79
80
// Functional operations
81
val doubled = result1.map(_ * 2) // Good(10)
82
val recovered = result2.getOrElse(-1) // -1
83
84
// Chaining operations
85
def parseAndDivide(xStr: String, yStr: String): Int Or String = {
86
for {
87
x <- parseIntSafely(xStr)
88
y <- parseIntSafely(yStr)
89
result <- divide(x, y)
90
} yield result
91
}
92
93
def parseIntSafely(str: String): Int Or String = {
94
try Good(str.toInt)
95
catch { case _: NumberFormatException => Bad(s"Not a valid integer: $str") }
96
}
97
```
98
99
### Attempt Function
100
101
Safe execution wrapper that catches exceptions and returns Or.
102
103
```scala { .api }
104
import org.scalactic._
105
106
/**
107
* Returns the result of evaluating the given block f, wrapped in a Good, or
108
* if an exception is thrown, the Throwable, wrapped in a Bad.
109
*/
110
def attempt[R](f: => R): R Or Throwable
111
```
112
113
**Attempt Examples:**
114
```scala
115
import org.scalactic._
116
117
// Safe division
118
val result1 = attempt { 10 / 2 } // Good(5)
119
val result2 = attempt { 10 / 0 } // Bad(ArithmeticException)
120
121
// Safe file operations
122
def readFileSafely(filename: String): String Or Throwable = {
123
attempt {
124
scala.io.Source.fromFile(filename).mkString
125
}
126
}
127
128
// Chaining with other Or operations
129
val processedResult = for {
130
content <- readFileSafely("data.txt")
131
lines = content.split("\n")
132
firstLine <- if (lines.nonEmpty) Good(lines.head) else Bad("File is empty")
133
} yield firstLine.toUpperCase
134
```
135
136
### Equality System
137
138
Pluggable equality for custom comparison logic.
139
140
```scala { .api }
141
import org.scalactic._
142
143
// Core equality trait
144
trait Equality[T] {
145
def areEqual(a: T, b: Any): Boolean
146
}
147
148
// Normalization trait
149
trait Uniformity[T] extends Equality[T] {
150
def normalized(a: T): T
151
def normalizedCanHandle(b: Any): Boolean
152
def normalizedOrSame(b: Any): Any
153
}
154
155
// Explicitly object for controlling implicit conversions
156
object Explicitly {
157
val decided: Decided.type = Decided
158
val after: After.type = After
159
}
160
161
// Custom equality examples
162
implicit val stringEquality = new Equality[String] {
163
def areEqual(a: String, b: Any): Boolean =
164
b match {
165
case s: String => a.toLowerCase == s.toLowerCase
166
case _ => false
167
}
168
}
169
```
170
171
**Equality Examples:**
172
```scala
173
import org.scalactic._
174
import org.scalactic.StringNormalizations._
175
176
// Case-insensitive string equality
177
implicit val caseInsensitiveEquality = new Equality[String] {
178
def areEqual(a: String, b: Any): Boolean =
179
b match {
180
case s: String => a.toLowerCase == s.toLowerCase
181
case _ => false
182
}
183
}
184
185
// Using with ScalaTest matchers
186
"Hello" should equal("HELLO")(decided by caseInsensitiveEquality)
187
188
// Normalization-based equality
189
implicit val trimmedStringEquality = StringNormalizations.trimmed
190
191
" hello " should equal("hello")(after being trimmed)
192
193
// Custom numeric tolerance
194
implicit val doubleEquality = TolerantNumerics.tolerantDoubleEquality(0.01)
195
3.14159 should equal(3.14)(decided by doubleEquality)
196
```
197
198
### Validated Types (AnyVals)
199
200
Type-safe wrappers for constrained values.
201
202
```scala { .api }
203
import org.scalactic.anyvals._
204
205
// Positive integers
206
final class PosInt private (val value: Int) extends AnyVal
207
object PosInt {
208
def from(value: Int): PosInt Or One[ErrorMessage]
209
def ensuringValid(value: Int): PosInt
210
}
211
212
// Non-zero integers
213
final class NonZeroInt private (val value: Int) extends AnyVal
214
object NonZeroInt {
215
def from(value: Int): NonZeroInt Or One[ErrorMessage]
216
def ensuringValid(value: Int): NonZeroInt
217
}
218
219
// Positive doubles
220
final class PosDouble private (val value: Double) extends AnyVal
221
object PosDouble {
222
def from(value: Double): PosDouble Or One[ErrorMessage]
223
def ensuringValid(value: Double): PosDouble
224
}
225
226
// Non-empty strings
227
final class NonEmptyString private (val value: String) extends AnyVal
228
object NonEmptyString {
229
def from(value: String): NonEmptyString Or One[ErrorMessage]
230
def ensuringValid(value: String): NonEmptyString
231
}
232
233
// Non-empty collections
234
final class NonEmptyList[+T] private (val value: List[T]) extends AnyVal
235
object NonEmptyList {
236
def from[T](value: List[T]): NonEmptyList[T] Or One[ErrorMessage]
237
def apply[T](head: T, tail: T*): NonEmptyList[T]
238
}
239
```
240
241
**Validated Types Examples:**
242
```scala
243
import org.scalactic.anyvals._
244
245
// Safe construction with validation
246
val posInt1 = PosInt.from(5) // Good(PosInt(5))
247
val posInt2 = PosInt.from(-5) // Bad(One("-5 was not greater than 0"))
248
249
// Unsafe construction (throws on invalid)
250
val posInt3 = PosInt.ensuringValid(10) // PosInt(10)
251
252
// Using in function parameters
253
def calculateArea(width: PosDouble, height: PosDouble): Double = {
254
width.value * height.value
255
}
256
257
val width = PosDouble.from(5.0).getOrElse(PosDouble.ensuringValid(1.0))
258
val height = PosDouble.from(3.0).getOrElse(PosDouble.ensuringValid(1.0))
259
val area = calculateArea(width, height)
260
261
// Non-empty collections
262
val nonEmptyList = NonEmptyList(1, 2, 3, 4) // NonEmptyList(List(1, 2, 3, 4))
263
val fromList = NonEmptyList.from(List(1, 2)) // Good(NonEmptyList(List(1, 2)))
264
val fromEmpty = NonEmptyList.from(List.empty) // Bad(One("List was empty"))
265
266
// Working with validated strings
267
def processName(name: NonEmptyString): String = {
268
s"Hello, ${name.value}!"
269
}
270
271
NonEmptyString.from("Alice") match {
272
case Good(name) => processName(name)
273
case Bad(error) => s"Invalid name: ${error.head}"
274
}
275
```
276
277
### Requirements and Assertions
278
279
Design by contract utilities for preconditions and postconditions.
280
281
```scala { .api }
282
import org.scalactic._
283
284
object Requirements {
285
// Precondition checking
286
def require(condition: Boolean): Unit
287
def require(condition: Boolean, message: => Any): Unit
288
289
// Null checking
290
def requireNonNull(reference: AnyRef): Unit
291
def requireNonNull(reference: AnyRef, message: => Any): Unit
292
293
// State checking
294
def requireState(condition: Boolean): Unit
295
def requireState(condition: Boolean, message: => Any): Unit
296
}
297
298
// Chain type (deprecated, use NonEmptyList)
299
@deprecated("Use NonEmptyList instead")
300
type Chain[+T] = NonEmptyList[T]
301
```
302
303
**Requirements Examples:**
304
```scala
305
import org.scalactic.Requirements._
306
307
class BankAccount(initialBalance: Double) {
308
require(initialBalance >= 0, "Initial balance cannot be negative")
309
310
private var balance = initialBalance
311
312
def withdraw(amount: Double): Unit = {
313
require(amount > 0, "Withdrawal amount must be positive")
314
requireState(balance >= amount, "Insufficient funds")
315
balance -= amount
316
}
317
318
def deposit(amount: Double): Unit = {
319
require(amount > 0, "Deposit amount must be positive")
320
balance += amount
321
}
322
323
def getBalance: Double = balance
324
}
325
326
// Usage
327
val account = new BankAccount(100.0)
328
account.deposit(50.0) // OK
329
account.withdraw(75.0) // OK
330
// account.withdraw(100.0) // Would throw IllegalStateException
331
```
332
333
### Snapshots and Pretty Printing
334
335
Enhanced error messages and value formatting.
336
337
```scala { .api }
338
import org.scalactic._
339
340
// Pretty printing customization
341
trait Prettifier {
342
def apply(o: Any): String
343
}
344
345
object Prettifier {
346
val default: Prettifier
347
val truncateAt: Int => Prettifier
348
val lineSeparator: String => Prettifier
349
}
350
351
// Position tracking for better error messages
352
package object source {
353
case class Position(fileName: String, filePath: String, lineNumber: Int)
354
355
implicit def here: Position = macro PositionMacro.genPosition
356
}
357
```
358
359
### Collection Utilities
360
361
Additional collection operations and utilities.
362
363
```scala { .api }
364
// Every type for expressing "all elements match"
365
final class Every[+T] private (underlying: Vector[T]) {
366
def map[U](f: T => U): Every[U]
367
def flatMap[U](f: T => Every[U]): Every[U]
368
def filter(p: T => Boolean): Seq[T]
369
def exists(p: T => Boolean): Boolean
370
def forall(p: T => Boolean): Boolean
371
def toVector: Vector[T]
372
def toList: List[T]
373
def toSeq: Seq[T]
374
}
375
376
object Every {
377
def apply[T](firstElement: T, otherElements: T*): Every[T]
378
def from[T](seq: Seq[T]): Every[T] Or One[ErrorMessage]
379
}
380
381
// One type for single-element collections
382
final class One[+T](element: T) {
383
def map[U](f: T => U): One[U]
384
def flatMap[U](f: T => One[U]): One[U]
385
def head: T
386
def toSeq: Seq[T]
387
def toList: List[T]
388
def toVector: Vector[T]
389
}
390
```
391
392
## Package Object Utilities
393
394
```scala { .api }
395
package object scalactic {
396
// Type aliases
397
type ErrorMessage = String
398
399
// Utility functions
400
def attempt[R](f: => R): R Or Throwable
401
402
// Version information
403
val ScalacticVersion: String
404
405
// Deprecated aliases (use new names instead)
406
@deprecated("Use NonEmptyList instead")
407
type Chain[+T] = NonEmptyList[T]
408
409
@deprecated("Use NonEmptyList instead")
410
val Chain = NonEmptyList
411
412
@deprecated("Use anyvals.End instead")
413
val End = anyvals.End
414
}
415
```
416
417
## Integration with ScalaTest
418
419
Scalactic integrates seamlessly with ScalaTest for enhanced testing capabilities:
420
421
```scala
422
import org.scalatest.flatspec.AnyFlatSpec
423
import org.scalatest.matchers.should.Matchers
424
import org.scalactic._
425
426
class ScalacticIntegrationSpec extends AnyFlatSpec with Matchers {
427
"Or type" should "work with ScalaTest matchers" in {
428
val result: Int Or String = Good(42)
429
430
result shouldBe a[Good[_]]
431
result.isGood should be(true)
432
result.get should be(42)
433
}
434
435
"Custom equality" should "be usable in tests" in {
436
implicit val caseInsensitive = new Equality[String] {
437
def areEqual(a: String, b: Any): Boolean =
438
b match {
439
case s: String => a.toLowerCase == s.toLowerCase
440
case _ => false
441
}
442
}
443
444
"Hello" should equal("HELLO")(decided by caseInsensitive)
445
}
446
447
"Validated types" should "ensure constraints" in {
448
val posInt = PosInt.from(5)
449
posInt shouldBe a[Good[_]]
450
posInt.get.value should be(5)
451
452
val invalidPosInt = PosInt.from(-1)
453
invalidPosInt shouldBe a[Bad[_]]
454
}
455
}
456
```