Attempts
A Dynamic holder of a potential value
Attempts in BoxLang are an enhanced Java Optional. It acts as a fluent container of a value or expression that can be null, truthy, falsey, or exist. It then provides fluent methods to interact with the potential value or expression.
Attempts are also unmodifiable, so you can chain methods to handle the value more functionally, but it never mutates the original value. It can also be seeded with validation information to create validation pipelines.
In this example, you can see that the expression creates a value to check if the user requested exists and is loaded. Then, you can fluently populate and save the user if the user is present or throw an exception.
BoxLang internally will return Attempts
in many BIFS and internal operations.
Why
Attempts will allow you to use more declarative and functional programming techniques than dealing with nulls or falsey values. It provides a better API for developers to follow and absorb. You will ultimately write concise and more readable code that is easier to maintain and test.
States
An attempt can only have two states: present or empty. They can be retrieved with two methods:
isEmpty():boolean
isPresent():boolean
The rules for evaluating that we have a value present are:
The value is not
null
If the value is castable to BoxLang Boolean, is it
true
Queries, structs, strings, arrays are castable to boolean. So if they are empty, the attempt is false.
If not castable, we have a value, so it's present.
We also have another method called isNull(),
which specifically checks whether the value is null
only!
Creation
You can create attempts using our BIF attempt()
.
Empty
To create empty attempts, don't pass anything:
Remember that this is an empty attempt, you can change it's value.
With Potential Value
If you pass a value into the BIF, that value will be stored in the attempt, which can later be evaluated for existence. You can pass a value or an expression that could be null
.
Usage
We can interact with it now that we have created an attempt or received one. Here is the arsenal of methods available to create fluent execution chains.
equals( object ):boolean
This allows you to compare if two attempts are equal.
filter( function ):attempt
If a value is present and matches the given closure/lambda, it returns an Attempt describing the value; otherwise, it returns an empty Attempt.
flatMap( function ):attempt
If a value is present, it returns the result of applying the given Attempt
-bearing mapping function to the value; otherwise, it returns an empty Attempt
. Using flatMap
allows you to avoid nested attempt objects and directly get the transformed result. The getEmail()
method returns an attempt, not a value.
get():any
Get the value of the attempt. If the attempt is empty it will throw an exception.
getOrDefault( other ):any / orElse( other )
If a value is present, returns the value, otherwise returns the other passed value passed. You can use the getOrDefaul() , orElse()
function according to your readability needs.
ifEmpty( consumer ):attempt / ifFailed()
If the attempt is NOT present, run the consumer. This returns the same attempt. Please note that you can use the ifEmpty() or the ifFailed()
alias, in order to improve your readability.
ifPresent( action ):attempt / ifSuccessful()
If a value is present, performs the given action with the value, otherwise does nothing. You can use either ifPresent() or ifSuccessful()
depending on your fluency
ifPresentOrElse( action, action ):attempt
If a value is present, performs the given action with the value, otherwise performs the given empty-based action.
map( mapper ):attempt
Map the attempt to a new value with a supplier if it exists, else it's ignored and returns the same attempt.
or( supplier ):attempt
If a value is present, returns the Attempt, otherwise returns an Attempt produced by the supplying function. This is great for doing n-tier level lookups.
orElseGet( supplier ):any
This is similar to the getOrDefault(), orElse()
methods, but with the caveat that this method calls the supplier closure/lambda, and whatever that produces is used. This is great for dynamically producing the result.
orThrow( [throwable|message] ):any
If a value is present, returns the value, otherwise throws a NoElementException
if no exception is passed. If you pass in a message, it will throw an exception with that message. If you pass in your own Exception object, it will throw that exception object.
stream()
If a value is present, returns a sequential Stream containing only that value, otherwise returns an empty Stream. Let's say we have a list of Person
objects, each with an optional Address
field. We want to extract a list of all city names from those persons who actually have an address.
toString()
Returns the string representation of the value, if any.
Validation Usage
The usage section focused on traditional usage for the attempt class. In this section, we will expand the usage to also include custom validation. The process of validation is:
Use the
to{Method}()
matchers to register what the value should match against.Validate using
isValid():boolean
to see if the value matches your validation matcher.Use the
ifValid( consumer )
that if the attempt is valid it will call your closure/lambda with the value of the attempt.Use the
ifInvalid( action )
that if the attempt is invalid it will call the action
If the state of the attempt is empty, then isValid()
will always be empty.
Matchers
The available matchers are:
toBe( other )
- Stores a value to explicitly match againsttoBeBetween( min, max )
- Validates the attempt to be between a range of numbers This assumes the value is a number or castable to a number. The range is inclusive/boxed.toBeType( type )
- Validates the attempt to be a specific BoxLang type that you can pass to theisValid
function. Check out the isValid functiontoMatchRegex( pattern, [caseSensitive=true] )
- Validates the attempt to match a regex pattern with case sensitivity This assumes the value is a string or castable to a stringtoSatisfy( predicate )
- Register a validation function to the attempt. This function will be executed when the attempt is evaluated It must return TRUE for the attempt to be valid. This is the most flexible approach as your closure/lambda will validate the incoming result attempt as it sees fit.
The matcher registration can happen at any time as long as it is before an isValid()
call.
Last updated