Repository: scikit-hep/awkward

Issue:
`ak.transform` doesn't respect `regular_to_jagged` for single argument
### Version of Awkward Array

main

### Description and code to reproduce

This simple demo does not produce a ragged array:
```python
import awkward as ak
import numpy as np

x = ak.transform(
    lambda layout, depth, continuation, **kwargs: continuation(),
    ak.from_numpy(np.arange(2 * 3 * 4 * 5).reshape(2, 3, 4, 5)),
    regular_to_jagged=True,
    numpy_to_regular=True
)
x.show(type=True)
```

Yet, I would expect it to, because of `regular_to_jagged` and `numpy_to_regular`. It is not documented that `regular_to_jagged` is only implemented for multiple array arguments, and we also implement `numpy_to_regular` for the single-array variant. Therefore, I suspect this is an omission rather than a policy choice.


Discussion:

Resolving patch:
diff --git a/src/awkward/_do.py b/src/awkward/_do.py
--- a/src/awkward/_do.py
+++ b/src/awkward/_do.py
@@ -27,6 +27,7 @@ def recursively_apply(
     return_simplified: bool = True,
     return_array: bool = True,
     function_name: str | None = None,
+    regular_to_jagged=False,
 ) -> Content | Record | None:
 
     if isinstance(layout, Content):
@@ -40,6 +41,7 @@ def recursively_apply(
                 "allow_records": allow_records,
                 "keep_parameters": keep_parameters,
                 "numpy_to_regular": numpy_to_regular,
+                "regular_to_jagged": regular_to_jagged,
                 "return_simplified": return_simplified,
                 "return_array": return_array,
                 "function_name": function_name,
diff --git a/src/awkward/contents/content.py b/src/awkward/contents/content.py
--- a/src/awkward/contents/content.py
+++ b/src/awkward/contents/content.py
@@ -9,7 +9,7 @@
 import awkward as ak
 from awkward._backends import Backend
 from awkward.forms.form import Form, _parameters_equal
-from awkward.typing import Any, AxisMaybeNone, Self, TypeAlias
+from awkward.typing import Any, AxisMaybeNone, Self, TypeAlias, TypedDict
 
 np = ak._nplikes.NumpyMetadata.instance()
 numpy = ak._nplikes.Numpy.instance()
@@ -29,6 +29,16 @@
 ]"""
 
 
+class RecursivelyApplyOptionsType(TypedDict):
+    allow_records: bool
+    keep_parameters: bool
+    numpy_to_regular: bool
+    regular_to_jagged: bool
+    return_simplified: bool
+    return_array: bool
+    function_name: str | None
+
+
 class Content:
     is_numpy = False
     is_unknown = False
@@ -1033,7 +1043,7 @@ def _recursively_apply(
         depth: int,
         depth_context: dict | None,
         lateral_context: dict | None,
-        options: dict[str, Any],
+        options: RecursivelyApplyOptionsType,
     ) -> Content | None:
         raise ak._errors.wrap_error(NotImplementedError)
 
diff --git a/src/awkward/contents/regulararray.py b/src/awkward/contents/regulararray.py
--- a/src/awkward/contents/regulararray.py
+++ b/src/awkward/contents/regulararray.py
@@ -1245,6 +1245,11 @@ def _drop_none(self):
     def _recursively_apply(
         self, action, behavior, depth, depth_context, lateral_context, options
     ):
+        if options["regular_to_jagged"]:
+            return self.to_ListOffsetArray64(False)._recursively_apply(
+                action, behavior, depth, depth_context, lateral_context, options
+            )
+
         if self._backend.nplike.known_shape:
             content = self._content[: self._length * self._size]
         else:


Test patch:
diff --git a/tests/test_2047_ak_transform_regular_to_jagged.py b/tests/test_2047_ak_transform_regular_to_jagged.py
new file mode 100644
--- /dev/null
+++ b/tests/test_2047_ak_transform_regular_to_jagged.py
@@ -0,0 +1,50 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
+
+import numpy as np
+import pytest  # noqa: F401
+
+import awkward as ak
+
+
+def test():
+    array = ak.from_numpy(np.arange(2 * 3 * 4 * 5, dtype=np.int64).reshape(2, 3, 4, 5))
+    assert array.type == ak.types.ArrayType(
+        ak.types.RegularType(
+            ak.types.RegularType(
+                ak.types.RegularType(ak.types.NumpyType("int64"), 5), 4
+            ),
+            3,
+        ),
+        2,
+    )
+
+    def apply(layout, **kwargs):
+        return
+
+    result_regular = ak.transform(
+        apply,
+        array,
+        numpy_to_regular=True,
+    )
+    assert result_regular.type == ak.types.ArrayType(
+        ak.types.RegularType(
+            ak.types.RegularType(
+                ak.types.RegularType(ak.types.NumpyType("int64"), 5), 4
+            ),
+            3,
+        ),
+        2,
+    )
+
+    result_ragged = ak.transform(
+        apply,
+        array,
+        regular_to_jagged=True,
+        numpy_to_regular=True,
+    )
+    assert result_ragged.type == ak.types.ArrayType(
+        ak.types.ListType(
+            ak.types.ListType(ak.types.ListType(ak.types.NumpyType("int64")))
+        ),
+        2,
+    )