Blog

BoxLang 1.0.0 Beta 6 Launched

Luis Majano July 19, 2024

Spread the word

Luis Majano

July 19, 2024

Spread the word


Share your thoughts

We are pleased to announce the release of BoxLang 1.0.0-Beta 6! This latest beta version includes improvements and essential bug fixes.

What is BoxLang?

BoxLang is a modern dynamic JVM language that can be deployed on multiple runtimes: operating system (Windows/Mac/*nix/Embedded), web server, lambda, iOS, android, web assembly, and more. BoxLang combines many features from different programming languages, including Java, ColdFusion, Python, Ruby, Go, and PHP, to provide developers with a modern and expressive syntax.

It is also a drop-in replacement for Adobe ColdFusion and Lucee Engines.

How to get started?

Visit our docs at https://boxlang.ortusbooks.com and get coding today. If you want to try it out on the web then go to our online REPL at https://try.boxlang.io. You can also checkout our YouTube playlist: https://www.youtube.com/playlist?list=PLNE-ZbNnndB-40LvAbeSeT2Oi3V2gm_B8

Release Notes

Here are the latest release notes: https://boxlang.ortusbooks.com/readme/release-history/1.0.0-beta-6

New Features

BL-157 Implement nested transactions

We are excited to bring the completion of nested transactions into BoxLang, something we have wanted in an open-source engine for years.

transaction{
  queryExecute( "INSERT INTO developers ( id, name, role ) VALUES ( 22, 'Brad Wood', 'Developer' )", {} );
    
    transaction{
      queryExecute( "INSERT INTO developers ( id, name, role ) VALUES ( 33, 'Jon Clausen', 'Developer' )", {} );
	 transactionRollback();
    }
    
}

Here, the INSERT in the child transaction is rolled back, but the parent transaction's INSERT statement persists.

General behavior:

  • A rollback on the parent transaction will roll back the child transaction.
  • A rollback on the child will NOT roll back the parent.
  • Child transaction savepoints use a prefix so they don't collide with the parent transaction.
  • Logging has been added, so you can easily see the transaction lifecycle as a transaction is created, savepoints are created, transactions rollback, etc.
  • You can't change any transaction properties on a nested transaction. i.e., once the parent transaction begins, you can't begin a child transaction and change the isolation level.

Transaction events will come in the next beta.

BL-348 queryReverse() finalized

This BIF is now complete so you can easily reverse query data.

BL-349 Allow servlet to expand paths so CommandBox web aliases or ModCFML web dirs can be used and new OnMissingMapping event.

Adobe CFML Engines do this today via their “magic” connector. Lucee has never supported it. The expanded path should work like so now:

  • if it doesn’t start with a / (and isn’t absolute), then make it relative to the base path
  • look for a BoxLang mapping (not including “/” )
  • Check if it’s an absolute path already, like C:/foo/bar
  • Then announce an ON_MISSING_MAPPING interception point
  • if no interceptor provided a value, then resolve via the root / mapping

The servlet runtime will have a listener for ON_MISSING_MAPPING that attempts to use servetContext.getRealPath() to try and resolve the path. This will allow any aliases in the resource manager to kick in.

If the servlet has an alias called /foo and you expand the path /foo/existingFile.txt, it will see it, but if you expand the path /foo/notExistingFile.txt then it will not “see” the mapping. We can solve that by taking off segments to see if any part of the path is found, but there is a performance hit to that, and I’m not sure if it’s necessary or not.

BL-350 Allow static functional access to BIFs

We have introduced static access to headless BIFS in BoxLang so you can use them as method references anywhere a BIF can be received, EVEN in Java classes that implement similar BIFS.

// Syntax
::BIF

// Examples
::UCase
::hash

This expression represents a first-class BoxLang function that can be invoked directly.

(::reverse)( "darb" ); // brad

You can also store the BIF static access into variables.

foo = ::ucase
foo( "test" ) // TEST

You can also leverage it as a high-order function combination from methods or classes that expect function references.

["brad","luis","jon"].map( ::ucase ); // [ "BRAD","LUIS","JON" ]

[1.2, 2.3, 3.4].map( ::ceiling );    // [2,3,4]

["brad","luis","jon"].map( ::hash ); // ["884354eb56db3323cbce63a5e177ecac", "502ff82f7f1f8218dd41201fe4353687", "006cb570acdab0e0bfc8e3dcb7bb4edf" ]

BL-351 Allow Functional binding to member functions

In order to explain this new functionality in BoxLang, here is a Java snippet:

List.of("apple", "banana", "cherry").stream().forEach(String::toUpperCase);

This syntax is a shortcut for writing out the following Java lambda:

List.of("apple", "banana", "cherry").stream().forEach(str -> str.toUpperCase());

(Other JVM languages like Clojure have similar syntax) The String::toUpperCase binds to the instance method toUpperCase, but in a way that it will be called on each string instance in the stream. So for each iteration, it will call the toUpperCase method on that instance. Nothing like this ever existed in CFML engines. However, in BoxLang, it does now. BoxLang allows you to do the following:

  • BoxLang-type member methods
  • Class member methods
  • Java member methods
  • Any object member/field

Since member methods in BoxLang aren’t necessarily bound to a class and we’re a dynamic language, we have simplified the syntax to this:

.methodName

This is a function expression that accepts a single argument and calls the method name specified on the incoming instance, returning the result of the member call. it is a shortcut for

i -> i.methodName()

So our Java example becomes like this in BoxLang

["apple", "banana", "cherry"].stream().forEach( .toUpperCase );

When not using (), check if there is a field of that name that is not a function, and if so, return that field value. So, the following example would fetch the name key in the struct.

nameGetter = .name
data = { name : "brad", hair : "red" }
nameGetter( data ) // brad

If the syntax .foo() is used or there is no field of that name, then it will be assumed we’re invoking a method. It can be directly invoked:

(.reverse)( "darb" ); // brad

It can be placed into a variable and invoked later:

foo = .ucase;
foo( "test" );  // TEST

And it can be passed to higher-order functions. (This is the main use case)

["brad","luis","jon"].map( .toUpperCase ); // ["BRAD","LUIS","JON"]

[1.2, 2.3, 3.4].map( .ceiling ); // [2,3,4]

[
  { myFunc : ()->"eric" },
  { myFunc : ()->"gavin" }
].map( .myFunc ) // [ "eric", "gavin" ]

Additionally, we have added support for multiple-arguments as well:

.methodName( arg1, arg2 )

Example:

["brad","luis","jon"].map( .left(1) ); // [ "b", "l", "j" ]

The arguments will not be evaluated until the function is invoked, and the argument expressions will be re-evaluated for every invocation.

pendingOrders.each( .submit( generateNextOrderNumber() ) ) // Fresh order number for each submission

The argument evaluation will have lexical binding to the declaring context.

local.suffix = " Sr."
["brad","luis","jon"].map( .concat( suffix ) ); // [ "brad Sr.", "luis Sr.", "jon Sr." "]

or

children.each( .setParent( this ) )

Bound member methods can also use named params (unless they are Java methods, of course)

foo = .left( count=2 );
foo( "test" ); // "te"

This brings a whole new dimension of dynamic goodness for not only BoxLang functions but also for Java functions. Welcome to a new era!

BL-352 arrayRange() BIF to create arrays with a specific range of values

This is one of our very first steps in supporting range types. You know have the capability to generate arrays with a specific boxed range using arrayRange( from, to ). You can use a from and to numeric index, and it will build the array with that many elements with the range as its value. However, you can also use range notation: {from}..{to}

// an array from 1 to 100
a = arrayRange( "1..100" )
a = arrayRange( 1, 100 )

// a negative indexed array
a = arrayRange( "-10..10" )
a = arrayRange( -10, 10 )

BL-353 threadTerminate() finalized

We have now finalized a threadTerminate( name ) BIF.

BL-354 threadJoin() BIF Finalized

We have now finalized a threadJoin( [name], timeout ) BIF. We have expanded this BIF and if you don't pass a thread name or a list of names, it will join all active threads.

BL-355 queryInsertAt() BIF finalized

This is now finalized.

BL-356 QueryRowSwap() bif finalized

This is now finalized.

BL-358 runAsync() completed but based on the powerful new BoxFuture -> CompletableFuture

This is a big ticket. Welcome to the future with BoxFuture. We have completed the ability to do asynchronous pipelines and parallel computations in BoxLang. We have introduced a new type called BoxFuture, which extends a Java Completable Future. The return of a runAsync() is a BoxFuture, and you can create pipelines to process the computations. You can access all the methods in a Completable future and many more. This will be documented in our Async Programming guide.

Futures

You have a new BIF: futureNew( [value], [executor] ) that can create new box futures. By default it will create incomplete and empty futures. However, you can pass different values to the future. Check out the API Docs: https://s3.amazonaws.com/apidocs.ortussolutions.com/boxlang/1.0.0-beta6/ortus/boxlang/runtime/async/BoxFuture.html

  • value : If passed, the value to set on the BoxFuture object as completed, or it can be a lambda/closure that will provide the value, and it will be executed asynchronously, or it can be a native Java CompletableFuture
  • executor : If passed, it will use this executor to execute the future. Otherwise, it defaults to the fork/join pool in Java.
// incomplete future
f = futureNew()

// completed future with a value
f = futureNew( myValue )

// a future that executes an asynchronous lambda/closure and the value will be the result
f = futureNew( () -> orderService.calculate() )

// A future with an executable and a virtual thread executor
f = futureNew(
    () -> processTasks(),
    executorNew( "virtual", "virtual" )
)

{% embed url="https://s3.amazonaws.com/apidocs.ortussolutions.com/boxlang/1.0.0-beta6/ortus/boxlang/runtime/async/BoxFuture.html" %}

runAsync( callback, [executor ] )

You can pass a closure or lambda to runAsync() to execute it asynchronously. The seconds parameter is an executor which runs the threads. It runs in the fork/join pool by default, but you can also pass your own executors. The return is a BoxFuture, which you can then do asynchronous pipelines.

calculation = runAsync( () -> calculateNumber() )
    .then( result -> result * 2 )
    .then( result -> result + 5 )
    .onError( exception -> { manage exception } )
    .get()
    

// Use a custom executor
runAsync( () -> "Hello", executorNew( "single", "single" ) )
    .then( ::println )
    .join()
    
// Process an incoming order
runAsync( () => orderService.getOrder() )
    .then( order => enrichOrder( order ) )
    .then( order => performPayment( order ) )
    .thenAsync( 
        order => dispatchOrder( order ), 
        executorNew( "cpuIntensive", "fixed", 150 )
     )
    .then( order => sendConfirmation( order ) );
 
// Combine Futures
var bmi = runAsync( () => weightService.getWeight( rc.person ) )
    .thenCombine(
	runAsync( 
	    () => heightService.getHeight( rc.person ) ),
            ( weight, height ) => {
            var heightInMeters = arguments.height/100;
            return arguments.weight / (heightInMeters * heightInMeters );
            })
        .get();   

BL-359 BIF Collection for managing and interacting with async service executors: list, get, has, new, shutdown

You now have the full capability to register, create, and manage custom executors in BoxLang. The AsyncService manages all executors in BoxLang.

Here are the new functions dealing with executors:

  • executorGet( name ): Get a registered executor
  • executorHas( name ): Checks if an executor has been registered or not
  • executorList() : Lists all registered executors
  • executorNew( name, type, [maxThreads:20] ): Creates and registers an executor by type and max threads if applicable.
  • executorShutdown( name ): Shuts down an executor
  • executorStatus( [name] ): Get a struct of executor metadata and stats by name or all.

The available executor types are:

  • cached - Creates a cached thread pool executor.
  • fixed - Creates a fixed thread pool executor.
  • fork_join - Creates a fork-join pool executor.
  • scheduled - Creates a scheduled thread pool executor.
  • single - Creates a single thread executor.
  • virtual - Creates a virtual thread executor.
  • work_stealing - Creates a work-stealing thread pool executor.

BL-360 Configuration now supports executor registration for global usage

You can now register your executors globally in BoxLang in the boxlang.json

// Global Executors for the runtime
// These are managed by the AsyncService and registered upon startup
// The name of the executor is the key and the value is a struct of executor settings
// Types are: cached, fixed, fork_join, scheduled, single, virtual, work_stealing
// The `threads` property is the number of threads to use in the executor. The default is 20
// Some executors do not take in a `threads` property
"executors": {
	"boxlang-tasks": {
		"type": "scheduled",
		"threads": 20
	},
	"cacheservice-tasks": {
		"type": "scheduled",
		"threads": 20
	}
},

BL-124 Implement primary/foreign key columns in dbInfo

DBInfo now returns primary and foreign key columns when you need them.

BL-255 Implement QueryMeta class for $.bx.meta or $.bx.getMeta() debugging support

Metaprogramming on queries is now available with much more information like caching, execution status, records, etc.

Improvements

BL-357 "Fix" return types of BIFs that return "true" for no real reason

One of the odd things about CFML is the following:

myArr.append( "foo" ) // returns myArr
arrayAppend( myArr ) // returns true??

In BoxLang, the following BIFS returns the instance it was called with instead of a boolean. However, if you are in compat mode in a CFML file, you will get the old behavior.

  • arrayAppend()
  • arrayClear()
  • arrayDeleteAt()
  • arrayInsertAt()
  • arrayResize()
  • arraySet()
  • arraySwap()
  • StructClear()
  • StructKeyTranslate()
  • StructInsert()
  • structAppend()
  • QuerySetRow()
  • QueryDeleteRow()
  • QuerySort()
  • ArrayPrepend()

The following BIFs return something other than the original data structure, but they have a good reason for doing so, so they remain as is:

  • queryAddColumn() -- returns index of the column removed
  • queryAddRow() -- returns the row number added

The following BIF returns the query in Adobe, which we’ll match. Lucee returns the array of removed data, but we’re ignoring that since we agree with Adobe’s behavior.

  • QueryDeleteColumn()

BL-366 Provide Java FI typing on all higher-order BIFs

BL-371 make name un-required in the application component

BL-346 DynamicObject equals() hashCode() to allow for dynamic checking

Bugs

BL-364 Overridden getter in parent class not being used

BL-370 List and Delmiter Args are not correct in ListReduce callback

BL-365 The throwable Dump table is not styled

Add Your Comment

Recent Entries

Why BoxLang When You Have Kotlin, Groovy, Scala, and more…

Why BoxLang When You Have Kotlin, Groovy, Scala, and more…

As we approach a stable release of BoxLang and our continued marketing reaches more folks, many have asked about its purpose. Why create a new language when the JVM ecosystem already includes established languages like Kotlin, Groovy, and Scala, to name a few.

Luis Majano
Luis Majano
December 18, 2024
ColdBox Free Tip 6 - Using Routing with Wildcard Domains!

ColdBox Free Tip 6 - Using Routing with Wildcard Domains!

ColdBox gives you the flexibility to create domain-specific routes, making it perfect for multi-tenant applications or projects that need to respond differently based on the domain or subdomain being accessed. In this tip, we’ll dive into how to use the withDomain() method to create routes that match specific domains or sub-domains.

Maria Jose Herrera
Maria Jose Herrera
December 18, 2024